Cocoaプログラミング:クラスクラスタとクラスの継承
元ネタは黒本より。
Cocoaをやっていてクラスの継承をやってみようとしても、クラスクラスタの場合だと、すんなりとは継承できないです。
例えば、NSStringを継承して、文字列を逆に入れ替えるメソッドを持つ、ReversibleStringを実装してみます。
単純に考えると、
#import <Foundation/Foundation.h> @interface ReversibleString : NSString -(id) reversedString; @end @implementation ReversibleString: - (id) reversedString { unichar *buffer, temp; unsigned long length, i; id reversed; length = [self length]; if (length <= 0){ goto err; } buffer = (unichar *) malloc(length * sizeof(unichar)); if (buffer == nil){ goto err; } // bufferにcontentの内容を埋める [self getCharacters:buffer range:NSMakeRange(0, length)]; // bufferの前後の文字を順々に入れ替えて行く for (i = 0; i < length/2; i++){ temp = buffer[i]; buffer[i] = buffer[(length-1)-i]; buffer[(length-1)-i] = temp; } // bufferを元にreversedオブジェクトを作成 reversed = [[self class] stringWithCharacters:buffer length:length]; free(buffer); err: return reversed; } int main (int argc, const char * argv[]) { char buf[100]; id s, a, b, c, d, e; @autoreleasepool { scanf("%s", buf); s = [[NSString alloc] initWithUTF8String:buf]; a = [[ReversibleString alloc] initWithString:s]; printf("a => %s\n", [a UTF8String]); b = [[ReversibleString alloc] initWithString:@"Reverse? "]; c = [b stringByAppendingString:[a reversedString]]; printf("c => %s\n", [c UTF8String]); d = [[ReversibleString alloc] initWithString:c]; e = [b stringByAppendingString:[d reversedString]]; printf("e => %s\n", [e UTF8String]); } return 0; }
と、reversedStringのように、追加メソッドのみを実装すればよい気がしますが、実行すると、
hello 2012-01-22 08:55:54.922 reversibleString[328:707] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** initialization method -initWithCharactersNoCopy:length:freeWhenDone: cannot be sent to an abstract object of class ReversibleString3: Create a concrete instance!' *** First throw call stack: ( 0 CoreFoundation 0x00007fff84f92286 __exceptionPreprocess + 198 1 libobjc.A.dylib 0x00007fff87a24d5e objc_exception_throw + 43 2 CoreFoundation 0x00007fff84f920ba +[NSException raise:format:arguments:] + 106 3 CoreFoundation 0x00007fff84f92044 +[NSException raise:format:] + 116 4 Foundation 0x00007fff8e5b476b _NSRequestConcreteObject + 101 5 Foundation 0x00007fff8e5eb855 -[NSString initWithCharactersNoCopy:length:freeWhenDone:] + 20 6 Foundation 0x00007fff8e5ec265 -[NSString initWithString:] + 172 7 reversibleString 0x0000000100000fbc main + 364 8 reversibleString 0x0000000100000e44 start + 52 9 ??? 0x0000000000000001 0x0 + 1 ) terminate called throwing an exception
と、しょっぱなのinitWithStringの呼び出しで例外エラーが発生します。これは、NSStringクラスが「クラスクラスタ」だからです。
クラスクラスタとは?
クラスクラスタとはその名の通り、複数のクラスの集合体です。クラスクラスタのオブジェクトは、外部から見るとNSStringクラスにしか見えないのに、実際には別のサブクラスのインスタンスになることがあります。外部にしか見えないクラスを「パブリッククラス」、実際のクラスを「コンクリートクラス」と呼ぶそうです。
例えば、クラスを調べて出力するprintClass()を用意し、
#import <Foundation/Foundation.h> static void printClass(NSString *s) { printf("class=%s, member=%s, kind=%s\n", [NSStringFromClass([s class]) UTF8String], [s isMemberOfClass:[NSString class]] ? "YES" : "NO", [s isKindOfClass:[NSString class]] ? "YES" : "NO"); return; } int main (int argc, const char * argv[]) { char buf[100]; id s; @autoreleasepool { scanf("%s", buf); s = [NSString alloc]; printClass(s); s = [s initWithUTF8String:buf]; printClass(s); } return 0; }
実行してみると、
hello class=NSPlaceholderString, member=NO, kind=YES class=__NSCFString, member=NO, kind=YES
のように、メソッドが呼ばれる度に異なるコンクリートクラスのインスタンスへと変わって行くことが分かると思います。なかなか変態チックです。
クラスクラスタを継承するには?
クラスクラスタは複数のサブクラスで成り立っていることから、個々のクラス毎に異なる部分とクラスタ内で共通している部分とで分かれます。さらに、個々のクラス毎に異なる部分へのアクセスを担当するメソッドを「プリミティブメソッド」と呼びます。
このクラスクラスタを継承するには、プリミティブメソッドとプライベートなデータ構造を再定義する必要があります。
黒本によると、具体的なクラスクラスタの継承には以下の定石を踏む必要があります。
定石に従って実装
という訳で、ReversibleStringクラスを定石通り実装してみると、以下のようになりました。
#import <Foundation/Foundation.h> @interface ReversibleString : NSString { NSString *content; // NSStringクラスのインスタンスを内部に持つ } - (id) initWithString:(NSString *)aString; - (id) initWithCharacters:(const unichar *)characters length:(NSUInteger)length; + (id) stringWithString:(NSString *)string; + (id) stringWithCharacters:(const unichar *)characters length:(NSUInteger)length; - (NSUInteger) length; - (unichar) characterAtIndex:(NSUInteger)index; - (id) reversedString; @end @implementation ReversibleString // 指定イニシャライザ - (id) initWithString:(NSString *)aString { self = [super init]; if (self != nil){ content = aString; } return self; } - (id) initWithCharacters:(const unichar *)characters length:(NSUInteger)length { self = [super init]; if (self != nil){ content = [[NSString alloc] initWithCharacters:characters length:length]; } return self; } // コンビニエンスコンストラクタ + (id) stringWithString:(NSString *)string { return [[[self class] alloc] initWithString:string]; } + (id) stringWithCharacters:(const unichar *)characters length:(NSUInteger)length { return [[[self class] alloc] initWithCharacters:characters length:length]; } // プリミティブメソッド - (NSUInteger) length { return [content length]; } - (unichar) characterAtIndex:(NSUInteger)index { return [content characterAtIndex:index]; } - (id) reversedString { /* 前と同じです */ } @end int main (int argc, const char * argv[]) { /* 前と同じです */ }
かなりめんどくさいです。。。
実行してみると、
hello a => hello c => Reverse? olleh e => Reverse? hello ?esreveR
今度は上手く動きました。つまり、OOPで言うところのis-aではなくhas-aの関係を作らないといけません。(これって継承というより委譲?)
カテゴリを使って実装する
ちなみに、こういう場合はカテゴリを使ってNSStringクラスにメソッドを追加する形で実装した方が、圧倒的に楽です。
#import <Foundation/Foundation.h> @interface NSString (Reversible) -(id) reversedString; @end @implementation NSString (Reversible) - (id) reversedString { /* 前と同じ */ } @end int main(void) { /* 前と同じ */ }
で同じように動きます。
でも、上記のカテゴリでは独自のインスタンス変数が追加できないんですよね。なので、そういうときはやっぱり継承するしかない。でもスーパークラスがクラスクラスタだと面倒くさい。。。まあそういうものなのかなぁ。もっといいやり方があれば教えて下さい(>_<)
場合によってはメッセージ転送まで実装する必要がある?
色々と調べていたら、以下のようなページを見つけました。
クラスクラスタによっては、(void)forwardInvocation:まで実装しないといけないとなると、かなり大変ですね。(というか、やりたくないな。。。)