objective c 동적결합
아래 로직은 터미널에 입력된 숫자에 따라 변수 obj에 대입되는 객체 클래스가 달라진다. 클래스 A, B는 whoAreYou에 대응되지만, NSObejct는 대응못한다.
objective c에서 어떤 객체가 그 메시지를 처리할수 있는지 없는지는 실제로 메시지를 보낼 때 정해진다. 모든 인스턴스는 자신이 어떤 클래스 인스턴스인지 알고 있으므로 메시지를 받았을 때 그에 따라 처리한다. 반대로 객체가 받은 메시지를 처리 못할때는 아래와 같이 실행 에러가 발생하고 종료된다.
이렇게 송신된 메시지에 대응해서 어떤 메서드가 실행될지가 실행 시에 결정되는 방식을 동적결합이라고 한다.
#import <Foundation/NSObject.h>
#import <stdio.h>
@interface A : NSObject
- (void)whoAreYou;
@end
@implementation A
- (void)whoAreYou { printf("I'm A\n"); }
@end
@interface B : NSObject
- (void)whoAreYou;
@end
@implementation B
- (void)whoAreYou { printf("I'm B\n"); }
@end
int main(void)
{
id obj;
int n;
scanf("%d", &n);
switch (n) {
case 0: obj = [[A alloc] init]; break;
case 1: obj = [[B alloc] init]; break;
case 2: obj = [[NSObject alloc] init]; break; //오류!
}
//호출
[obj whoAreYou];
return 0;
}
결과
다형성이란?
위의 예제에서 obj에 대입된 인스턴스가 클래스 A, 클래스 B 어느쪽이라도 같은 메시지에 반응했지만, 그 반응은 대입된 인스턴스에 의존한다. 이처럼 같은 메시지를 보내더라도 리시버 객체에 따라 적절한 메서드가 선택되어 실행되는 성질을 다형성이라고 부른다.
객체는 메시지에 반응하는 '물건' 이므로 사용자는 내부 구현이 어떻게 되었는지 몰라도 된다. 즉, 어떤 메시지에 반응해서 정해진 동작을 한다는 것만 알고 있으면 다른 클래스 인스턴스더라도 같은 방법으로 다룰 수 있다.
예) 도형이 마우스로 드래그 될 때의 처리
target = 도형
절차적 프로그래밍
switch(target->kind){
case Line :
lineDragged(drection);
case Circle :
circleMove(drection);
break;
}
객체지향 프로그래밍
변수 target이 다형성을 구현한 시스템의 객체라면 아래와 같이 처리할 수 있다. 각각의 도형이 이동 될 때, 어떤 처리를 할지는 실제로 메시지를 받은 객체 target이 결정하면 된다.
[target move:direction];
만약 switch 문을 사용해서 만든다면 새로운 도형이 추가될 때마다 새로운 분기를 추가해야 한다.
형식과 클래스란?
많은 객체지향 언어는 클래스와 자료형을 동일시 한다. 지금까지는 객체는 id 형으로 나타낸다고 설명했는데, 특정 클래스의 인스턴스라는 걸 지정할 수도 있다.
Foo *
이 형식은 객체가 대입되는 변수, 메서드와 함수의 인수, 반환값형으로 사용할 수 있다.
Volum *v;
빈 포인터 nil은 무엇인가?
객체가 포인터로 표현된다고 하면, '빈' 포인터에 대응하는 것도 있다. objective c에서는 어떤 객체도 나타내지 않는, 즉 포인터가 가리키는 실체가 없다는 걸 뜻하는 nil이라는 값이 있다. nil은 id형의 빈포인터로, 값은 0이다. 초기자가 초기화에 실패했을 때 돌려주는 값도 nil 이다.
인스턴스 객체를 새로 작성할 때, 메서드 alloc은 인스턴스 변수를 0으로 초기화하지만 id형이나 그 외의 객체를 나타내는 포인터는 nil로 초기화 한다. id형을 돌려주는 메소드 중 상당수는 처리가 정상적으로 끝나지 않았을때, 값으로 nil을 반환한다.
형식을 정적으로 확인하려면?
객체는 id형으로 선언해도 된다. 하지만 클래스명을 명시적으로 선언하면 컴파일할 때 형식 확인이 가능하다. 구문에서 선언한 클래스와 사용한 객체의 클래스가 일치 하지 않으면 경고가 나온다.
objective c 에서 사용하는 클래스가 정해져 있으면 클래스명을 명시해서 형식 선언을 하면 컴파일할 때 메시지가 처리 가능한지를 확인한다. 따라서 의도하지 않은 클래스를 잘못 사용하는 위험성이 낮아진다.
id형을 사용해서 확장성이 있는 유연한 프로그램을 만들 것인가 아니면 클래스명을 명시해서 안전성이 높은 프로그램을 만들것인가는 프로그래머에게 달려 있다.
정적형식 확인 정리
캐스트를 사용한 예제
슈퍼클래스와 서브클래스 사이의 형식 관계에서 캐스트를 사용해 컴파일러를 속여야 하는 경우가 있다. 그런 전형적인 예로 슈퍼 클래스 형식을 가진 변수에 서브 클래스의 인스턴스를 대입해서 사용하는 것이다.
@interface AudioPlayer : NSObject{
Volume *theVolume;
}
@end
위 클래스를 상속한 CDPlayer 클래스에 음소거 기능을 넣고 싶다. 그래서 Volume 클래스의 서브 클래스인 MuteVolume 클래스의 인스턴스를 theVolume에 대입한다. 이 인스턴스는 AudioPlayer의 다른 부분에서는 Volume 클래스 인스턴스처럼 다루므로 문제없이 동작한다.
@implement CDPlyaer
-(id) init{
if((self = [super init]) != nil){
theVolume = [[MuteVolume alloc] init];
}
}
단 한 곳, 음소거 기능을 사용하는 부분만큼은 캐스트를 사용해야 한다. 왜냐면 슈퍼 클래스 Volume에는 mute 메소드가 없기 때문에 캐스트를 해주어야 한다.
-(void)mute : (id) sender{
[(MuteVolume * )theVolume mute]
}
예제파일
인스턴스 변수에 접근하는 방법?
기본적으로 objective c는 객체 밖에서는 인스턴스 변수에 접근하지 못한다. 하지만 클래스 A 메서드 정의 안에서 클래스 A의 self 이외의 인스턴스가 가진 인스턴스 변수에는 직접 접근할 수 있다.
그러나 접근 할 수 있는가는 형식 확인과 마찬가지로 컴파일 할 때 확인한다. 따라서 인스턴스 변수에 접근하려면 정적으로 해당 인스턴스 객체를 클래스명을 형식으로 사용하는 형식 선언해야 한다.
아래는 삼원색을 나타내는 클래스로, 두 색의 중간색을 계산하는 기능만 있다.
삼원색 클래스 인터페이스
#import <Foundation/NSObject.h>
@interface RGB : NSObject
{
unsigned char red, green, blue;
}
- (id)initWithRed:(int)r green:(int)g blue:(int)b;
- (id)blendColor:(RGB *)color;
- (void)print;
@end
삼원색 클래스 구현 부분
#import "RGB.h"
#import <stdio.h>
//지역함수
static unsigned char roundUChar(int v)
{
if (v < 0) return 0;
if (v > 255) return 255;
return (unsigned char)v;
}
@implementation RGB
//슈퍼 클래스의 지정초기자에서 NSObject의 init 보조초기자 호출 가능
- (id)initWithRed:(int)r green:(int)g blue:(int)b
{
if ((self = [super init]) != nil) {
red = roundUChar(r);
green = roundUChar(g);
blue = roundUChar(b);
}
return self;
}
- (id)blendColor:(RGB *)color
{
red = ((unsigned int)red + color->red) / 2;
green = ((unsigned int)green + color->green) / 2;
blue = ((unsigned int)blue + color->blue) / 2;
return self;
}
- (void)print {
printf("(%d, %d, %d)\n", red, green, blue);
}
@end
실행부분
#import "RGB.h"
int main(void)
{
RGB *u, *w;
u = [[RGB alloc] initWithRed:255 green:127 blue:127];
w = [[RGB alloc] initWithRed:0 green:127 blue:64];
[u print];
[w print];
[[u blendColor: w] print];
return 0;
}
결과
메서드 blendColor: 는 인수 객체에 -> 를 써서 인스턴스 변수를 직접 참조한다. 이런 방법으로 접근이 가능한건 인수가 같은 클래스 인스턴스 이기 때문이다. 다른 클래스 인스턴스는 슈퍼 클래스 인스턴스라도 이 방법으로는 참조하지 못한다. 또한 클래스명으로 형식을 선언하지 않았을때도 접근이 불가능 하다.
접근자란?
objective c 프로그램은 인스턴스 객체 속성을 조사하거나 값을 변경할 때 인스턴스 변수에 직접 접근하는 방법을 사용하지 않는다. 보통은 인스턴스 변수에 접근하는 메서드를 정의해서 사용한다.
예를 들어 weight 라는 float형의 속성이 있다면 이것을 참조하기 위한 인스턴스 변수와 같은 이름의 메서드를 준비한다.
-(float) weight
또한 이 속성값을 변경하는 메서드 명은 인스턴스 변수명 앞에 set을 붙이고 set 다음에 오는 글자를 대문자로 한다.
-(void) setWeight : (float) value
이렇듯 인스턴스 변수를 참조하거나 변경하기 위해 정의한 메서드를 접근자메서드 라고 한다. 값을 참조하는 메서드를 게터, 값을 변경하는 메서드를 세터 메서드라고 한다.
왜 이런 접근자를 사용하나?
객체 정보 은폐를 위해서. 어떤 인스턴스 변수가 있고 어떻게 사용되는가는 클래스 구현 방법에 따라서 많이 차이가 난다. 인스턴스 변수에 접근하도록 허용하면 그 뒤로는 클래스 구현 방법이 바뀌어 인스턴스 변수가 없어지거나 역할이 바뀌면 그 클래스 인스턴스를 사용하는 다른 모듈 동작이 어떻게 변할지 모른다.
이에 비해 속성 접근을 메서드 호출이라는 형태로 작성하면 클래스 구현 방법이 바뀌어도 속성에 접근하는 메서드만 변경하면 대응할 수 있다.
인스턴스 변수의 가시성이란?
인스턴스 변수를 외부에 어떻게 노출할 것인가. 보이게 할 것인가, 보인다면 그 범위는 어디까지 할 것인가를 가시성이라고 한다.
가시성 지정은 클래스 인터페이스의 인스턴스 변수 선언에서 이루어진다.
구현 부분에 인스턴스 변수를 정의하면?
어떤 인스턴스 변수를 정의해서 클래스를 구현하는지에 대한 정보를 클래스 외부에 알릴 필요가 없다며 인스턴스 변수를 구현부분에 작성해도 된다. 클래스 구현 방법이 변경되어 인스턴스 변수 정의가 바뀌더라도 헤더 파일을 변경하지 않아도 된다.
하지만 슈퍼 클래스가 이 방법으로 정의 되어 있으면 서브 클래스에서 인스턴스 변수가 보이지 않게 된다! 서브 클래스에서 인스턴스 변수가 나타내는 정보에 접근하려면 접근자나 선언 프로퍼티를 사용해야 한다.
클래스 객체란 무엇인가?
objective c에서는 클래스 객체를 팩토리 메소드라고 부른다. 인스턴스가 가진 변수를 인스턴스 변수라고 하고, 인스턴스가 사용하는 메소드를 인스턴스 메소드라고 부르는 것처럼 클래스 메소드는 클래스가 사용하는 메서드이다. objective c에서는 클래스 메소드는 있어도, 클래스 변수는 없다. 클래스 객체 자체는 프로그램을 실행하면 처음부터 자동으로 클래스마다 하나씩 존재해서, 클래스 객체를 따로 생성할 필요는 없다.
클래스 객체 형식은?
클래스 객체도 클래스 변수에 대입할 수 있다. 객체는 id형으로 나타낼 수 있다. 클래스 객체도 마찬가지로 id형으로 다룰 수 있으며 객체용으로 특별히 Class 형이 있다. Class 형도 id 형과 같이 실제로는 포인터지만 실체가 무엇을 가리키고 있는지 의식할 필요는 없다. 또한 빈 포인터를 나타내기 위해 Nil 값이 있다. Nil은 Class 형의 빈 포인터로, 실제로 값은 0 이다.
NSObject 클래스에는 class 라는 메서드가 있다. 이 클래스 메서드는 모든 클래스가 사용할 수 있어서 그 클래스의 클래스 객체를 결과값으로 돌려준다.
클래스 객체 사용할때 주의점은?
우선, 클래스 메서드 안에서 인스턴스 변수를 참조못한다. 클래스 객체는 하나밖에 없지만 인스턴스는 여러 개 존재 할 수 있다. 따라서 클래스 메서드 속에서 인스턴스 변수를 참조하려고 해도 그것이 어떤 인스턴스에 있는지 알 수 없다. 마찬가지로 인스턴스 메소드를 호출하는 것도 안된다.
다음으로 클래스 메서드를 실행하는 중에 변수 self가 클래스 객체 자신을 참조 한다. 따라서 어떤 클래스 메서드 안에서 다른 클래스 메서드를 호출할 때는 self에 대해 클래스 메서드를 호출하는 메시지를 보내면 안된다.
클래스 변수 사용법은?
objective c는 구현 파일 속에 static 지정자와 함께 정의된 변수를 클래스 변수 대신 사용한다. 하지만 static 지정자를 붙여서 정의하면 해당 변수와 함수는 정의 파일 내부에서만 유효하다. 즉, 해당 클래스의 클래스 메소드와 인스턴스 메소드에서만 참조 가능하다.
문제는 상속을 사용해서 클래스를 정의할 때, 서브 클래스에서 static을 사용해서 정의한 변수를 사용할 수 없다. 따라서 위에서 언급한 대로, 클래스 객체 속성에 접근하려면 접근자 메서드를 클래스 메서드로 정의해야 한다. 서브 클래스에도 해당 메서드가 상속되므로 자동으로 슈퍼 클래스 내부의 지역 변수에 접근할 수 있다.
클래스 객체 초기화 방법은?
인스턴스 객체는 생성된 직후에 초기화를 위해 메시지를 보내면 된다. 하지만 클래스 객체는 프로그램이 실행되는 시점에 이미 존재하기 때문에 생성 직후 메시지를 보내는 것은 불가능 하다.
objective c에서는 루트 클래스 NSObject에 initialize 메서드가 있어서 이것으로 각 클래스를 초기화 한다.
프로그램이 실행되고 나서 어떤 클래스가 처음으로 메시지를 받으면 바로 initialize 메서드가 자동으로 실행되는데 그보다 먼저 슈퍼 클래스의 클래스 객체에도 initialize 메소드가 호출된다. 각 클래스의 initialize 메소드는 단 한번만 호출 된다. 즉, initialize 메서드에 작성된 코드는 클래스 객체 (및 인스턴스) 가 사용되기 전에 한 번만 호출되며 클래스 객체를 초기화 한다.
@interface A : NSObject
+ (void)initialize;
@end
@implementation A
+ (void)initialize { printf("I'm A\n"); }
@end
@interface B : A
+ (void)initialize;
+ (void)setMessage:(const char *)msg;
- (void)sayHello;
@end
static const char *myMessage = "Hello";
@implementation B
+ (void)initialize { printf("I'm B\n"); }
+ (void)setMessage:(const char *)msg { myMessage = msg; }
- (void)sayHello { printf("%s\n", myMessage); }
@end
int main(void)
{
id obj = [[B alloc] init];
[obj sayHello];
[B setMessage: "Have a good day!"];
[obj sayHello];
return 0;
}
'아이폰 개발 > Objective C' 카테고리의 다른 글
NSObject 1 (0) | 2020.12.28 |
---|---|
objective c 소유권 (0) | 2020.12.27 |
objective c 상속과 클래스 (0) | 2020.12.25 |
objective c 프로그램 (0) | 2020.12.25 |
objective c 객체 개념 (0) | 2020.12.25 |