@uents blog

Code wins arguments.

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

のように、メソッドが呼ばれる度に異なるコンクリートクラスのインスタンスへと変わって行くことが分かると思います。なかなか変態チックです。

クラスクラスタを継承するには?

クラスクラスタは複数のサブクラスで成り立っていることから、個々のクラス毎に異なる部分とクラスタ内で共通している部分とで分かれます。さらに、個々のクラス毎に異なる部分へのアクセスを担当するメソッドを「プリミティブメソッド」と呼びます。

このクラスクラスタを継承するには、プリミティブメソッドとプライベートなデータ構造を再定義する必要があります。

黒本によると、具体的なクラスクラスタの継承には以下の定石を踏む必要があります。

  • プライベートなデータ構造を決める
  • イニシャライザを定義する
    • init以外のスーパークラスのイニシャライザを継承してはいけません。つまり、init...メソッドは独自に実装する必要があります。ただし、プライベートなデータ構造がなければ実装は不要です
  • プリミティブメソッドを実装する
    • プリミティブメソッドを独自にオーバーライドして実装する必要があります
  • 必要であれば、コンビニエンスコンストラクタを実装する
  • 必要であれば、任意のメソッドをオーバーライドする

定石に従って実装

という訳で、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:まで実装しないといけないとなると、かなり大変ですね。(というか、やりたくないな。。。)