본문 바로가기
아이폰 개발/Objective C

NSObject 2

by 인생여희 2020. 12. 28.

이전 포스팅에서 세가지 새로운 변수 형식에 대해서 알아봤다.

 

1. 형식이 유동적인 인스턴스 객체 변수

 

2.형식이 고정적인 인스턴스 객체변수(클래스 이름). 예를들면 NSObject, NSString emd

 

3.클래스 객체를 지정할 수 있는 변수 Class

 

@select(셀렉터) 

이번에는 메소드 문법과 셀렉터 변수를 변수 형식으로 지정하여 동적프로그래밍을 구현하는 방법을 알아 본다. 셀렉터를 저장하기 위한 변수형은 SEL 형식이다. 이것은 @select(셀렉터) 형식으로 메소드를 변수로 지정할 수 있다.

 

        //변수형식 SEL로 변수 sel 선언
        SEL sel;
        
        //인수가 없는 메소드 지정
        sel = @selector(description);
        
        //인수가 없는 사용자 정의 메소드 지정
        sel = @selector(doMyWork);
        
        //하나의 인수를 사용하는 메소드 지정
        sel = @selector(isMemberOfClass:);

 

 

SEL 형식 변수를 설정하였으면, 이제 이 변수를 이용해서 메소드를 실행하는 법을 알아본다. 다음과 같이 SEL 형식 변수를 인자로 perfomSelector: 메소드를 실행하면 된다.

 

 

지정한 메시지를 수신자에게 발송하고 결과를 반환한다.

 

- (id)performSelector:(SEL)aSelector;

 

- (id)performSelector:(SEL)aSelector withObject:(id)object;

 

- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

 

id 형식에 어떠한 객체를 대입할 수도 있는것 같이, 객체에 어떤 메소드를 실행하게 하는 형식에도 자유로운 적용을 가능하게 하는 것은 프로그램의 운영과 관리에 많은 유동성과 확장성을 제공해 준다. 이러한 유동성을 obj c에서는 다형성이라고 한다.

 

 

performSelector: 메소드를 사용하게 되면서 드는 의문점

 

1.아직 정의되지 않은 메소드를 셀럭터 SEL 변수에 대입하면?

 

-> 어떤 메소드를 셀렉터 변수에 지정하려고 할때, 해당 메소드가 어느 클래스에도 정의되어 있지 않다면, Undeclared selector..라는 컴파일 오류가 발생한다. 어떤 클래스에서든 셀력터 변수에 지정할 메소드 이름이 한번은 정의되어 있어야 한다.

 

 

2.A 클래스에 -(void) doMyWork 이 정의되어 있고. B 클래스에는 정의되어 있지않다. 하지만 클래스 B의 인스턴스 b에게 [b performSelector:@selector(doMyWork)] 를 실행하면?

 

-> 만약 1번 조건을 만족했다면 컴파일할때 리시버와 형식검사는 이루어지지 않고, 런타임 오류만 발생한다. 

 

 

3. -(id) performSelector : 는 반환형이 id 형식인데, 그외에 다른 반환형 (void, BOOL) 등을 가지는 셀렉터를 실행하면 어떻게 처리가 되어야 할까?

 

->Objective C의 기본 반환형은 id 형식이기 때문에 performSelector: 메소드의 기본 반환형도 id 형식을 선호한다. 대신 다른 형식을 반환하기 위해서는 강제 형변환이 필요하다.

 

 

예제

 


#import <Foundation/Foundation.h>

@interface Child : NSObject
@end

@implementation Child
-(int)returnNumber
{
	return 245;
}
-(void)doMyWork
{
	printf("Do my work.\n");
	return;
}
@end

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
		id a = [[NSObject alloc] init];
		NSString *str;
		SEL sel;
		
		str = [a description];				            // 일반 메소드 호출 형식에 의해 description 결과 획득
		NSLog(@"일반 메소드 형식 : %p %@", str, str);			// str의 포인터 및 내용 표시
		
		sel = @selector(description);		            // SEL 형식으로 description 추출 및
		str = [a performSelector:sel];		            // 메소드 실행, 결과 획득
		NSLog(@"셀렉터 형식 : %p %@", str, str);	            // 위와 같이 수행결과 표시, str 포인터는 다르지만 내용은 동일
		
        
		id b = [[Child alloc] init];
        
        // 프로그램 전체에서 임의의 클래스에서 doMyWork이 정의된 경우 컴파일 경고 꺼짐
		sel = @selector(doMyWork);
		
        //함수의 프린트 값 Do my work. 를 출력하고 12 출력.
        printf("%d\n", (int)[b performSelector:sel]);
        
        
        //컴파일 오류: 컴파일러 자체가 메소드의 반환형식을 검사하기 때문이다.
		//printf("%d\n", (int)[b doMyWork]);// Operand of type 'void' where arithmetic or pointer type is required.
		
        
        // 인스턴스 a는 NSObject 소속으로 doMyWork이 미지정상태이다. 런타임 오류
        // unrecognized selector sent to instance 0x1001115b0 <- 인스턴스 a의 포인터
        //[a performSelector:sel];
                                        
		
        // sel에 BOOL 반환형의 메소드 지정
		sel = @selector(isMemberOfClass:);
		
        
        //반환형이 블리언형이라도 performSelector 에 전달된다.
        //반환형이 id형식인 performSelector도 간단히 원하는 형식으로 캐스트 하면 유효한 값을 받을 수 있다.
        printf("%d\n", (int)[b performSelector:sel withObject:[Child class]]);	// 실행 및 결과 출력
        printf("%d\n", (int)[b isMemberOfClass:[Child class]]);
		
        
        sel = @selector(returnNumber);		// sel에 int 반환형의 메소드 지정
		printf("%d\n", (int)[b performSelector:sel]);
	}
    return 0;
}

 

결과

 

 일반 메소드 형식 : 0x103a0c7f0 <NSObject: 0x103a0e4c0>
 셀렉터 형식 : 0x103bb0010 <NSObject: 0x103a0e4c0>
Do my work.
12
1
1
245
Program ended with exit code: 0

 

 

Target-Action [타겟 - 액션, 목표-동작] 패러다임

 

SEL 형식의 변수는 특정 객체에 전송할 메소드(메시지)를 유동적으로 변경하는데 사용한다. 이러한 기능은 앱을 제작할 때 GUI(윈도우, 버튼, 이미지, 텍스트 등) 간의 상호작용을 편리하게 구성할 수 있도록 해준다.

 

 

Cell 클래스는 다양한 상태를 화면에 보여주는 기능을 하며 이 상태를 전환시키기 위한 메소드를 가지고 있다.

 

또한 Command라는 클래스에는 메시지를 전송할 target이라는 목표 객체의 id 주소와 어떤 메소드를 실행할지를 알려줄 action 이라는 SEL 형식의 메소드 포인터를 가지고 있다. 이 Command 클래스에는 두 변수를 설정할 수 있는 메소드와 최종적으로 action을 target에서 실행하도록 하는 실행(performClick) 기능을 가지고 있다.

 

1.타겟 변수 지정

2.액션(selector) 지정

3.performSelector 실행

 

 

애플의 운영체제에서 화면을 설계할 때 동작하는 객체들은 위와 같이 타겟-액션 방법을 사용하여 동작을 수행한다. performSelector를 수행할 때 이 명령을 지시한 발신자 객체의 id를 함께 보내도록 한다. 이 발신자 또는 송신자 id는 후에 리시버가 명령을 어떻게 수행할지 상세한 정보를 담고 있을 수 있기 때문이다. 명령을 받는 객체를 수신자라고 하는 것과 같이 명령을 보내는 객체를 송신자라고 한다. 이런 이유로 가장 많이 사용하는 명령은 [receiver performSelector:aSelector withObject:sender] 가 된다.

 

 

타겟 액션 예제

 

#import <Foundation/Foundation.h>

@interface MyCell : NSObject
{char chState;}     //내부 상태를 저장하는 문자 변수 (-1, 0, 1)
-(id)init;
-(char)getState;     //내부 상태 참조를 위한 메소드
-(void)setActive;
-(void)setReady;
-(void)setDeactive;
@end

@implementation MyCell
-(id)init
{
	self = [super init];
	if(self) chState = 0;
	return self;
}
-(char)getState
{
	switch (chState)
	{
		case  0:return '_';
		case -1:return 'e';
		case  1:return 'E';
	}
	return '.';
}
-(void)setActive   { chState =  1; }
-(void)setReady    { chState =  0; }
-(void)setDeactive { chState = -1; }
@end

@interface MyCommand : NSObject
{
	SEL action;  //액션 지정용 - 기본값은 nil
	id target;  //타겟 지정용 - 기본값은 nil
}
//@property (readwrite, assign) SEL action;
//@property (readwrite, assign) id target;
-(void)setAction:(SEL)sel;
-(void)setTarget:(id)obj;
-(void)performClick:(id)sender;     //현재 설정된 타겟 - 액션 값으로 실제 실행명령
                                //performSelector 방식으로 동작
@end

@implementation MyCommand
//@synthesize action, target;
-(void)setAction:(SEL)sel { action = sel; }
-(void)setTarget:(id)obj  { target = obj; }
-(void)performClick:(id)sender
{ (void)[target performSelector:action withObject:sender]; }
@end

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
		char ch;
		id a = [[MyCell alloc] init];
		id b = [[MyCell alloc] init];
		id c = [[MyCell alloc] init];
		id button = [[MyCommand alloc] init];
		for(;;)
		{
			printf("\n%c %c %c\n", [a getState], [b getState], [c getState]);
			printf("Control : 1-a 2-b 3-c\n");
			printf("Action  : (q Active), (w Ready), (e Deactive)\n");
			printf("          (a Action), (z Quit)\n]");
			scanf("%c", &ch);
			switch (ch)
			{
                    
            /*
                    1 ~ 3 : 실행을 수행할 목표 설정 setTarget: 동작
                */
                    
				case '1':[button setTarget:a]; break;
				case '2':[button setTarget:b]; break;
				case '3':[button setTarget:c]; break;
                    
            /*
                    q ~ e : 실행을 수행할 동작 설정 setAction: 동작
               */
                    
				case 'q':[button setAction:@selector(setActive)]; break;
				case 'w':[button setAction:@selector(setReady)]; break;
				case 'e':[button setAction:@selector(setDeactive)]; break;
                    
            
            /*
                     현재 설정된 target 과 action 실제 실행
                */
                    
				case 'a':[button performClick:nil]; break;
				default:
					break;
			}
            
			if(ch == 'z') break;
		}
	}
    return 0;
}

 

결과

 

 

 

다형성

obj c에서는 클래스 이름에 포인터 형식(*)을 붙여 변수를 선언하면 이것은 정적 결합변수가 되며 id 형식으로 변수를 선언하면 이것은 동적 결합 변수가 된다. 정적결합 변수는 인스턴스의 설정이나 메소드의 실행시 컴파일 시간에 형식 검사를 수반한다. 반면 동적 결합 변수는 클래스 형식의 검사를 실행시간까지 전부 미루게 되며 모든 책임을 프로그래머에게 넘겨 준다. 동적 결합 변수인 id 포인터 방식은 포인터 연산자 *를 사용하지 않는다.

 

 

예제

 

#import <Foundation/Foundation.h>

@interface A : NSObject
-(void)print;
@end
@implementation A
-(void)print
{ printf("I'm a Class A.\n"); }
@end

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
        /*
         - 서로 다른 인스턴스 변수이지만 , 동일한 클래스의 인스턴스 세개 생성
         - obj c에서 상위 계층으로의 클래스 설정은 가능하고
           하위 계층으로의 설정은 오류를 유발한다.
         */
        
        
		id i = [[A alloc] init];
		NSObject *n = [[A alloc] init];
		A *a = [[A alloc] init];

        
        //하위 계층으로의 설정은 오류를 유발한다.
        //Incompatible pointer types initializing 'A *' with an expression of type 'NSObject *'
        A * aa =  [[NSObject alloc]init];
       
        
        
        
        /*
         클래스 A에 정의된 메소드 실행
         변수 i, n, a에 각각 print 메소드를 실행할 메시지를 보낸다.
         동적 결합을 사용하는 id 형식은 메소드가 실행가능한지 사전 점검을 하지 않는다.
         'NSObject' may not respond to 'print' 경고를 발생시킨다.
         경고가 발생하였지만 세변수 모두 클래스 A의 인스턴스 이므로 실행은 성공한다.
         */
		[i print];
		[n print];	// 경고, 'NSObject' may not respond to 'print'
		[a print];
		[i release]; [n release]; [a release];
		
        
        /*
         서로 다른 변수 형식간에 포인터 주소 설정 (assign)
         동적 결합인 id 형식을 사용한 변수 할당은 컴파일 시간에 형식 점검이 생략된다.
         그러나 정적 결합을 사용하는 인스턴스 변수 n과 a의 할당은 형식 점검을 한다.
         */
        
		i = a;
		a = i;
        
        //n = a의 경우 하위계층인 클래스 A의 변수가 상위계층인 클래스 NSObject로 할당되기 때문에 문제가 안된다.
		n = a;
        
		// 반대는 컴파일, 런타임 경고가 발생한다.
        // 경고, Incompatible pointer types assigning to 'A *' from 'NSObject *'
        a = n;
		
        a = nil;	    // 객체 포인터의 무력화
        
		//a = NULL;	// nil과 마찬가지로 동작, 그러나 비추
		
        [a print];	// nil 포인터에 발송하는 메세지는 동작하지 않는다.
	
    }
    return 0;
}

//결과
I'm a Class A.
I'm a Class A.
I'm a Class A.
Program ended with exit code: 0

 

 

결과

 

 

 

 

정적결합에서 발생할 수 있는 변수의 형식 선언과 실제 연결되는 인스턴스 형식 연결간에 차이가 발생할 때의 동작과 컴파일 경고를 피하기 위해 사용하는 캐스트 연산자와의 관계에 대해서 알아보자.

 

 

 

예제

 

#import <Foundation/Foundation.h>

@interface A : NSObject
-(void)who;
@end
@implementation A
-(void)who { printf("클래스 A의 인스턴스\n"); }
@end

@interface AA : A
-(void)who;
-(void)say;
@end
@implementation AA
-(void)who { printf("클래스 AA의 인스턴스\n"); }
-(void)say { printf("안녕하세요.\n"); }
@end

@interface B : NSObject
-(void)bravo;
@end
@implementation B
-(void)bravo { printf("Bravo\n"); }
@end

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
        
        
       
        
    
		A *a, *aa;
		B *b;
//		id b;			// 변수 b를 id 형식으로 바꾸면 컴파일 오류는 사라지지만 실행 오류 발생
		a = [[A alloc] init];
        
        //변수 aa는 클래스 A 형식이지만 이 변수에는 실제로 AA의 인스턴스를 배정하였다.
        //즉 상위 계층 클래스 형식으로 변수를 배정했다.
        //이런 경우 클래스 A의 메소드만 실행 시키면 문제는 없다.
        //클래스 AA의 메소드를 실행하려고 하면 컴파일 경고가 발생한다.
		aa = [[AA alloc] init];
		
        
        b = [[B alloc] init];
		[a who];
        
        //B클래스에는 who 메소드가 없어서 응답하지 않을 수 있다고 오류. 하지만 수행은 성공한다!
        // 컴파일 경고: 'B' may not respond to 'who'
		[(B *)a who];

        
    /*
         @참고@
     
         컴파일러는 그 변수의 형식과 대입되는 인스턴스의 형식을 검사한다.
         이때 상위 계층으로의 배정은 허가한다.
         또한 컴파일러는 해당 변수에 메소드를 실행할 때는 변수의 클래스 형식에 따라 적합한 메소드인지 검사한다.
         
         위의 경우 클래스 A의 메소드 who를 실행하는데,, 클래스 A의 인스턴스를 클래스 B 형식으로 캐스트하고
         수행하기 때문에 경고가 발생한다.
         그러나 실제 메소드를 실행할 때는 인스턴스의 isa를 참조하고 클래스 A의 메소드를 검색하여 who 실행에 성공한다.
    */
        
        
        /* A 슈퍼클래스에는 say 메소드가 없다.
             컴파일 경고는 없지만 실행오류 발생
             이 경우 say는 AA 의 메소드 이기 때문에 컴파일 경고는 발생하지 않는다.
             그러나 실제 실행시에 a의 isa는 클래스 A를 가리키고 있기 때문에
             say의 메소드는 미정상태가 되고, 실행 오류가 발생한다.*/
          
          //[(AA *)a say];
		
        
        
        /*
         클래스 A 형식의 변수 aa에 who 메소드를 실행했다.
         이때 클래스 A의 who 가 실행되는지, 클래스 AA의 오버라이드된 who가 실행되는지 주의한다.
         실제 실행되는 인스턴스의 메소드는 그 변수 형식에 따른 메소드가 아닌!
         실제 인스턴스의 isa 클래스 형식 메소드가 실행된다는 점 기억!
         */
        [aa who];		// 오버라이드된 who가 실행된다.
        
        
        /*
          변수 aa는 클래스 A 형식이고 say 메소드는 미정 상태이기 때문에 컴파일 경고는 발생.
         하지만 실제 인스턴스 클래스는 AA이고, say 메소드가 정의되어 있기 때문에 실행에 문제없다.
         컴파일 경고: 'A' may not respond to 'say', 실행
         */
      
		[aa say];
		
        
        [(B *)aa say];	// 컴파일 경고: 'B' may not respond to 'say', 실행
//		[b who];		// 컴파일 경고: 'B' may not respond to 'who', 실행오류 발생
		[a release];
		[aa release];
		[b release];
	}
    return 0;
}

 

정리

 

1. id 형식으로 선언한 변수에 대해서는 어떠한 메소드도 실행 요청할 수 있다. 단 호출되는 메소드는 사전에 최소 하나 이상의 클래스에서 정의되어 있어야 한다. 그렇지 않으면 컴파일 에러가 발생한다. 

 

또 메소드의 인자를 id 형식으로 정의하면 비록 같은 클래스의 인스턴스라고 하더라도 연산자 -> 멤버 변수에 접근할 수 없다.

 

 

 

 

2.id형식으로 선언한 변수와 클래스 타입으로 선언한 변수 간의 상호 설정은 상하위계층에 관계가 없으므로 언제나 가능하다. 또한 메소드의 인자로의 대입과 리턴값의 대입도 가능하다. 

 

 

 

 

3.클래스 타입으로 선언된 변수는 자신 클래스가 수행할 수 있는 메소드 이외에 메소드를 호출하면 컴파일 경고가 발생한다. 컴파일 경고가 발생하더라도 빌드는 가능하며, 실시간 오류는 프로그래머가 해결해야 한다.

 

 

4. 클래스 형식으로 선언한 변수에 그 클래스의 서브클래스의 인스턴스를 설정하는 것은 가능하다. 즉, 상위계층으로의 설정은 가능하다. 반대로 하위계층으로의 설정은 컴파일 경고를 유발한다. 그러나 상위계층으로 설정한 변수에 하위 인스턴스에만 존재하는 메소드를 실행하면 이것 역시 3번 규칙에 위배되므로 실행은 되지만 컴파일 경고가 유발된다. 

 

 

5.클래스 형식으로 선언한 변수에 그 클래스의 슈퍼 클래스의 인스턴스를 설정하면 경고를 유발한다. 4번과 반대되는 경우로 하위계층으로의 설정을 뜻한다. 만일 하위계층으로 설정한 변수에 하위 인스턴스만 존재하는 메소드를 실행하면 실시간 오류가 발생하기 때문이다. 

 

 

 

6.프로그램 코드에서 선언에 관계없이 실행 중에는 실제 변수에 들어 있는 isa 포인터가 가리키는 클래스에 저장된 메소드가 실행된다. 위의 실행 예제를 보면 변수 a의 형식은 클래스 A이지만 실제 배정된 인스턴스는 클래스 AA 형식이다. 때문에 [a who]를 실행하면 클래스 A의 who가 아닌 isa 포인터가 가리키는 클래스 AA의 오버라이드 된 who 메소드가 실행된다. 

 

 

 

'아이폰 개발 > Objective C' 카테고리의 다른 글

objective c 클래스 클러스터(추상클래스)  (0) 2020.12.29
objective c 카테고리  (0) 2020.12.29
NSObject 1  (0) 2020.12.28
objective c 소유권  (0) 2020.12.27
objective c 동적결합  (0) 2020.12.26