Subscribed unsubscribe Subscribe Subscribe

NSDictionaryのキーは変えられない(3) 無理矢理キーを変える

NSDictionaryのキーは変えられない - すぎゃーんメモ
NSDictionaryのキーは変えられない(2) 値は変えられる - すぎゃーんメモ
の続き。しつこくやってみる。
その1では、keyにNSMutableStringを入れて中身を変更しようとして失敗したのだけど、もっと単純なカスタムクラスを使ってしまえば実体がどうのこうのって話を回避することができるのではないか?と考えた。
というわけでkeyにするためのクラスを新たに作ってみることにした。単純にint型変数を持つだけのクラス。

#import <Foundation/Foundation.h>

@interface MyClass : NSObject {
    int num;
}

- (id)initWithNum:(int)arg;

@end


@implementation MyClass

- (id)initWithNum:(int)arg {
    if ((self = [super init]) != nil) {
        num = arg;
    }

    return self;
}

@end


int main(int argc, char *argv[])
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    MyClass *key1 = [[[MyClass alloc] initWithNum:1] autorelease];
    MyClass *key2 = [[[MyClass alloc] initWithNum:2] autorelease];
    MyClass *key3 = [[[MyClass alloc] initWithNum:3] autorelease];
    NSArray *keys = [NSArray arrayWithObjects:key1, key2, key3, nil];

    NSString *value1 = @"hoge";
    NSString *value2 = @"fuga";
    NSString *value3 = @"piyo";
    NSArray *values = [NSArray arrayWithObjects:value1, value2, value3, nil];

    NSDictionary *dict = [NSDictionary dictionaryWithObjects:values forKeys:keys];

    NSLog([dict description]);
    [pool release];
    
    return 0;
}

で、NSDictionaryを作って表示してみると…

$ ./a.out
2008-12-04 07:46:31.120 a.out[2018:10b] *** -[MyClass copyWithZone:]: unrecognized selector sent to instance 0x103480
2008-12-04 07:46:31.123 a.out[2018:10b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[MyClass copyWithZone:]: unrecognized selector sent to instance 0x103480'
2008-12-04 07:46:31.123 a.out[2018:10b] Stack: (
    2488357195,
    2443800123,
    2488386378,
    2488379724,
    2488379922,
    2488385786,
    2488332857,
    2488352216,
    2488343994
)

ぬぁ、落ちた!!
"copyWithZone:"というメソッドが勝手に送られているらしい。当然そんなものは実装していない。
が、リファレンスを読んでみると、そこら中に「keyにはNSCopyingProtocolをconformしろ」と書いてある。
NSDictionary Class Reference
どうやらNSDictionaryに格納される際にkeyはコピーされたものが使われるらしい。
というわけで以下のページを参考にしつつNSCopyingプロトコルを採用してMyClassを変更。
ダイナミックObjective-C (57) デザインパターンをObjective-Cで - Prototype (1) | マイナビニュース

@interface MyClass : NSObject <NSCopying> {
    int num;
}

- (id)initWithNum:(int)arg;

@end


@implementation MyClass

- (id)initWithNum:(int)arg {
    if ((self = [super init]) != nil) {
        num = arg;
    }

    return self;
}

- (id)copyWithZone:(NSZone *)zone {
    id newInstance = [[[self class] allocWithZone:zone] initWithNum:num];
    return newInstance;
}

@end

さぁ、これでどうだ!

$ ./a.out
2008-12-04 08:00:14.505 a.out[2083:10b] {
    <MyClass: 0x105b30> = fuga;
    <MyClass: 0x105b40> = piyo;
    <MyClass: 0x105af0> = hoge;
}

よし、エラーは起こらなくなった。
見にくいのでdescriptionをオーバーライドしてみよう。あとallKeysの内容を確認してみる。

@implementation MyClass

...

- (NSString *)description {
    return [NSString stringWithFormat:@"< %d >", num];
}

@end


int main(int argc, char *argv[])
{
...

    NSLog([dict description]);
    NSLog(@"%p, %@", keys, [keys description]);
    for (id key in keys) {
        NSLog(@"%p, %@, %@", key, key, [key className]);
    }
    NSLog(@"%p, %@", allkeys, [allkeys description]);
    for (id key in allkeys) {
        NSLog(@"%p, %@, %@", key, key, [key className]);
    }
...

で実行してみると

$ ./a.out
2008-12-04 08:04:27.634 a.out[2100:10b] {
    < 2 > = fuga;
    < 1 > = hoge;
    < 3 > = piyo;
}
2008-12-04 08:04:27.636 a.out[2100:10b] 0x104d50, (
    < 1 >,
    < 2 >,
    < 3 >
)
2008-12-04 08:04:27.636 a.out[2100:10b] 0x103480, < 1 >, MyClass
2008-12-04 08:04:27.637 a.out[2100:10b] 0x104d30, < 2 >, MyClass
2008-12-04 08:04:27.642 a.out[2100:10b] 0x104d40, < 3 >, MyClass
2008-12-04 08:04:27.643 a.out[2100:10b] 0x105d40, (
    < 2 >,
    < 1 >,
    < 3 >
)
2008-12-04 08:04:27.644 a.out[2100:10b] 0x105b40, < 2 >, MyClass
2008-12-04 08:04:27.645 a.out[2100:10b] 0x105b00, < 1 >, MyClass
2008-12-04 08:04:27.645 a.out[2100:10b] 0x105b50, < 3 >, MyClass

うん、やっぱり違うインスタンスたちが格納されているな。格納されるときにcopyWithZoneの結果を入れているっていうことか〜。
とは言え、それぞれの中の値は変更できるようにしてしまえば変更できるはず。試してみよう。

@implementation MyClass

...

- (void)setNum:(int)arg {
    num = arg;
}

@end


int main(int argc, char *argv[])
{
...

    NSLog([dict description]);
    for (id key in allkeys) {
        [key setNum:4];
    }
    NSLog([dict description]);

...

さぁ、どうなる!?

$ ./a.out
2008-12-04 08:11:17.306 a.out[2133:10b] {
    < 2 > = fuga;
    < 3 > = piyo;
    < 1 > = hoge;
}
2008-12-04 08:11:17.307 a.out[2133:10b] {
    < 4 > = fuga;
    < 4 > = piyo;
    < 4 > = hoge;
}

やった!!成功!?
さて、ここでnumに4を持つMyClassをキーとして指定してこのdictからオブジェクトを取ってくると何が返ってくるのか…!?

    for (id key in allkeys) {
        [key setNum:4];
    }
    NSLog([dict description]);
    MyClass *key4 = [[[MyClass alloc] initWithNum:4] autorelease];
    NSLog(@"return object:%@", [dict objectForKey:key4]);

結果は…

$ ./a.out
2008-12-04 08:16:51.251 a.out[2164:10b] {
    < 4 > = fuga;
    < 4 > = piyo;
    < 4 > = hoge;
}
2008-12-04 08:16:51.253 a.out[2164:10b] return object:(null)

nullだ orz
中に4が入っているからと言って、dict内に入っているキーたちと一致しているとは見なされないらしい。
もう一度NSDictionary Class Referenceを見てみると、
「(as determined by isEqual:).」という記述が。じゃあ isEqual: メソッドもオーバーライドしてしまえばいいんだなw
要は中のnumの値が同じかどうかだけで判断してしまえばいいんだから…

@implementation MyClass

...

- (int)num {
    return num;
}

- (BOOL)isEqual:(id)anObject {
    if (num == [anObject num]) {
        return YES;
    } else {
        return NO;
    }
}

@end

こんなカンジ?
で、さっきと同じものを実行してみると

$ ./a.out
2008-12-04 08:25:09.482 a.out[2209:10b] {
    < 4 > = fuga;
    < 4 > = hoge;
    < 4 > = hoge;
}
2008-12-04 08:25:09.484 a.out[2209:10b] return object:hoge

なんと!?piyoが消えたwwwwww
もうこうなってくると挙動が読めない。4を持つkeyに対する値でhogeが返ってくるならもうfugaにはアクセスできないか、と思いきや

    for (id key in [dict allKeysForObject:@"fuga"]) {
        NSLog(@"return object:%@", [dict objectForKey:key]);
    }

と書けば

2008-12-04 08:36:14.848 a.out[2431:10b] return object:fuga

と、しっかり得ることが出来てしまう。
何にせよ、NSDictionaryのエントリーそのものがkeyの重複によって消えてしまうようなことはないようだ。
うーん、結局結論が無い、というかよく分からないことになってしまった。。。
まぁどうせこんなワケの分からない操作を実際のアプリでするワケはないか。
こんなことができて、こういうことが起こる、というお話、とまとめよう。
『自分で作成したクラスを使えば、キーの重複のようなことを起こすことはできる。その際のNSDictionaryの挙動はよく分からなくなる。』
以上。