連想配列の扱い方2

前回「連想配列の扱い方1」に続いて、Objective-C連想配列のお話。

前回は、NSDictionaryとjava.util.Mapの違いを説明した。
今回は主にNSMapTableというクラスについて解説する。

NSMapTableは一言でいうと、チューニング可能なNSDictionaryといったイメージで扱うことができると思う。
汎用目的のNSDictionaryと言い換えてもよいだろう。


以下に、NSDictionaryとNSMapTableの挙動を対比してみる。


・NSDictionary / NSMutableDictionaryはキーをコピーする。
 そして値に対する強参照を保持する。

・NSMapTableは可変である。
 NSDictionaryに対応する不変クラスは存在しない。

・NSMapTableはキーや値をコピーせずに、強参照、あるいは弱参照することができる。

・NSMapTableは値をコピーして保持することもできる。

・NSMapTableは任意のポインタを格納できる。
 しかも、任意のポインタの等価性とハッシュコード算出を制御できる。




強参照と弱参照

以前、どこかの記事で、「メモリ管理のためにオブジェクトのオーナーシップを意識しなければならない」
という話をしたと思う。
NSArrayやNSDictionaryは、値を強参照(== retain)する。
即ち、NSArrayやNSDictionaryに要素を格納する場合、コンテナに値のオーナーシップを持たせることになる。
ちなみに、弱参照とは、retainせずに参照(ポインタの値)だけをコピーすることである。

NSMapTableは、キーと値、それぞれを強参照するのか、弱参照するのかをプログラマが指定できる。

例えば、UIViewをキーとして、そのビューがタップされた回数を管理する連想配列を構築したいとする。
ご存知のように、UIViewはCompositeパターンで設計されているので、UIViewのオーナーシップは親となるUIViewにあるべきだ。
そう考えると、この連想配列のキーはUIViewの弱参照であることが好ましい。
(親ビューよりも長期間に渡って連想配列を所有したいのであれば、強参照にすべきだが、ここでは弱参照で持つ方が都合がよいものとする。)
一方、値となる数値には、これといったオーナーがいないのであれば、連想配列の値はNSNumberの強参照とするのが都合がよい。

【コード例1】
キーを弱参照、値を強参照で保持する連想配列を構築する。

NSMapTable* map = [NSMapTable weakToStrongObjectsMapTable];
[map setObject:[NSNumber numberWithInt:0] forKey:view1];

このようにNSMapTableを構築した場合、mapはキー(UIView)をretainすることはないが、値(NSNumber)はretainする。
これは、mapがdeallocするときに、内部のすべての値(NSNumber)はreleaseされるということである。
もちろん、キーを弱参照するからには、mapの生存期間中、キーのオブジェクトが破棄(dealloc)されないことが前提である。

ちなみに、キーや値の取得方法は、NSDictionaryと同じである。

・objectForKey:でキーに対応する値を取得する
・keyEnumeratorですべてのキーを列挙する
・objectEnumeratorですべての値を列挙する




キーを弱参照した場合の不思議な挙動

コード例1では、weak-to-strong形式のマップテーブルを使ってみた。
ところが、APIリファレンスにはこんな記述がある。

Special Considerations
Use of weak-to-strong map tables is not recommended. The strong values for weak keys which get zeroed out continue to be maintained until the map table resizes itself.

訳:

特記事項
weak-to-strongのマップテーブルを使うことは推奨しません。強参照の値へのゼロアウトされた弱参照のキーは、マップテーブルが自身をリサイズするまでの間、保持され続けます。

「ゼロアウト・・・?」
私はこの注意書きを最初に読んだときに、意味がわからなかった。*1

結論からいうと、「推奨しない」とは書いてあるが、使用方法を間違えなければ、使っても全然大丈夫だ。
要は、弱参照しているキーがdeallocされてしまったときの挙動を説明しているわけで、通常はそのようなことは起きないはずだ。
なぜなら、キーを弱参照するのは、マップテーブル内にそのキーが保持されている間、キーとなるオブジェクトが生存しているのが明らかである場合であり、そうでないのならば強参照にすべきだからだ。

キーや値を弱参照するからには、マップテーブルの生存中に、キー、または値のどちらかが破棄(dealloc)されるならば、
プログラマの責任で、マップテーブルからそのエントリを削除すべきである。

注意書きに書かれていることは、以下のコードで説明できる。

【コード例2】

int main(int argc, const char** argv)
{
    @autoreleasepool {
        // weak-to-strongのマップテーブルを作る。
        NSMapTable* map = [NSMapTable weakToStrongObjectsMapTable];

        // キーとなるオブジェクトを生成する。
        id key = [[NSObject alloc] init];
        
        // キーに対して適当な値をマッピングする。
        [map setObject:[NSNumber numberWithInt:123] forKey:key];
        
        // ここで、キーになっている弱参照のオブジェクトを破棄(dealloc)する。
        // すると、マップテーブル内のキーの値が0になる。
        // これがリファレンスに書かれていたzeroed outである。
        [key release];
        key = [[[map keyEnumerator] allObjects] firstObject];
        NSLog(@"%p", key);            //=>0x0
        
        // マップ内にエントリはまだ存在している。
        NSLog(@"%lu", [map count]);   //=>1
        
        // リファレンスによると、マップのサイズが変更されるときに、0x0キーのエントリは削除されるらしい。
        // 試しに要素を1つずつ増やしていって、いつ削除されるのかを見てみる。
        int count = 1;
        
        for (int i = 0; 3000 > i; ++ i) {
            [map setObject:[NSNull null] forKey:[[[NSObject alloc] init] autorelease]];
            ++ count;
            
            // 私の環境では8回目でサイズ変更されたらしく、ゼロ要素は削除された。
            if ([map count] != count) {
                NSLog(@"%d回要素を追加したらマップが拡張しました。", count - 1);
                break;
            }
        }
    }
}

コピーをする、しないの制御

前回「連想配列の扱い方1」では、NSDictionaryはキーを「必ずコピーする」という話をした。
この仕様が弊害になることもあるだろうという説明もした。
一方、NSMapTableは、「強参照か弱参照か」だけでなく、「コピーするかしないか」についても、キーと値ごとに設定できる。
コード例1はキーも値もコピーしない指定になっている。
今度は、キーはコピーせずに、値のみをコピーする連想配列を定義してみる。

【コード例3】

// マップテーブルのキーとなるオブジェクトを自作
@interface MyObject : NSObject
@end

@implementation MyObject
// isEqualが呼び出されたときにログを出す
-(BOOL)isEqual:(id)object {
    NSLog(@"called isEqual method.");
    return [super isEqual:object];
}

// hashが呼び出されたときにログを出す
-(NSUInteger)hash {
    NSLog(@"called hash method.");
    return [super hash];
}
@end

// テスト用メイン関数
int main(int argc, const char** argv)
{
    @autoreleasepool {
        // キーを弱参照、等価性検証をポインタ比較とする
        // 値をコピーする
        NSMapTable* map = [[NSMapTable alloc] initWithKeyOptions:NSMapTableZeroingWeakMemory | NSMapTableObjectPointerPersonality
                                                    valueOptions:NSMapTableCopyIn capacity:0];

        // キーを2つ作る
        MyObject* key1 = [[[MyObject alloc] init] autorelease];
        MyObject* key2 = [[[MyObject alloc] init] autorelease];
        
        // 値を2つ作る
        // 一方は可変オブジェクト、もう一方は不変オブジェクトとする
        NSString* val1 = [NSMutableString stringWithString:@"1"];
        NSString* val2 = @"2";
        
        // キーと値をマッピングする
        [map setObject:val1 forKey:key1];
        [map setObject:val2 forKey:key2];

        // キーに対応する値を取得して、インスタンスレベルで同じであればコピーされていないと判定する
        // そうでなければ、コピーされていると判定する
        //--可変オブジェクトの場合は、以下のelseに入り、コピーされていることがわかる
        if ([map objectForKey:key1] == val1) {
            NSLog(@"val1 is not copy.");
        }
        else {
            NSLog(@"val1 is copy.");
        }
        
        //--不変オブジェクトの場合は、以下のifに入り、コピーされていないことがわかる
        if ([map objectForKey:key2] == val2) {
            NSLog(@"val2 is not copy.");
        }
        else {
            NSLog(@"val2 is copy.");
        }
    }
}

【実行結果】

val1 is copy.
val2 is not copy.

ハッシュ関数の制御

コード例3の実行結果を見ると、キーとなるMyObjectのhashメソッドが一切呼び出されていないことがわかる。
ハッシュテーブルの仕組み上、キーとなるオブジェクトのハッシュコードを計算する機構が必要である。
また、ハッシュコードがぶつかったときのために、キーが同じかどうかを調べる機構も必要である。
通常、その役割は、NSObject#isEqualとNSObject#hashが果たすのであるが、コード例3では呼び出されていない。
これはなぜか。
NSMapTableの構築時に、キーオプションとして、NSMapTableObjectPointerPersonalityを指定しているからである。
このオプションを指定すると、キーとなるオブジェクトは、「論理的等価性を持たない」ことを示すことができる。
この場合、オブジェクトの参照値(ポインタ)の値で比較が行われるし、ハッシュ値はオブジェクトへのポインタの値をシフトしたものが使用される。
試しに、コード例3のNSMapTableObjectPointerPersonalityオプションを削除して実行すると、hashメソッドが計4回(登録時と参照時に1回ずつ要素2つ分)呼び出されていることがわかる。




任意のポインタを格納できる

NSMapTableには、NSObjectへのポインタだけでなく、任意のCポインタをキーや値として登録することができる。
これについては、需要があまりなさそうなので、今回は割愛させていただく。

ところで、Objective-Cでは、Javaと違って、オブジェクト型(NSObject)ばかりを扱うわけではなく、Cの構造体などを使うこともままある。
たとえば、CGRect構造体やCGAffineTransform構造体や、あるいは自作した構造体など。
こういった構造体変数をコレクションの要素として扱いたい場合にどうするか。

まず、単純な数値(int, float, double, NSTimeIntervalなど)は、NSNumberクラスでラップしてやればよい。
構造体については、NSValueオブジェクトを使う。

【コード例4】
アフィン変換をNSMapTableの値として格納する。

NSMapTable* map = [NSMapTable weakToStrongObjectsMapTable];
CGAffineTransform transform = CGAffineTransformMakeScale(1.5f, 1.5f);
[map setObject:[NSValue valueWithCGAffineTransform:transform] forKey:self.view];

// 取り出したときはNSValueからCGAffineTransformに逆変換する。
transform = [[map objectForKey:self.view] CGAffineTransformValue];

CGRectやCGPointについても同じような変換メソッドが用意されているが、自作した構造体の場合は、専用の変換メソッドは存在しない。
そのような場合には、valueWithBytes:objCtype:を使うか(構造体メンバをすべてコピーする場合)、valueWithPointer:を使う(メモリを別途確保しておく場合)。

【参考】
http://blog.livedoor.jp/tek_nishi/archives/3474303.html

このテクニックはNSMapTableに限らず、NSArrayやNSDictionaryでも使えるので、前回言及しておくべきだった。




最後に、NSDictionaryのことも思い出してあげよう

前回「連想配列の扱い方1」と本記事をここまで読んでいただいた方はきっとこう思うに違いない。

「NSDictionaryなんていらない」
「NSMapTableだけ使えばいい」

一理ある。

しかし、NSCopyingの煩わしささえ避けられれば、NSDictionaryにもちょっとだけ素敵なところがある。

それは、要素へのアクセス時に、setObject:forKey:とobjectForKey:に対するシンタックスシュガーを使えるということだ。
たとえば、以下のコードをご覧いただきたい。

【コード例5】

int main(int argc, const char** argv)
{
    @autoreleasepool {
        NSMutableDictionary* dict = [NSMutableDictionary dictionary];
        
        // 要素を入れるのにこんなダサイ書き方せなあかんのか・・・
        [dict setObject:@"val" forKey:@"key"];
        
        // 要素の取得すら面倒な書き方だな・・・
        NSString* val = [dict objectForKey:@"key"];
        
        NSLog(@"%@", val);
    }
}

RubyJavaScriptを書いたことがある人からしたら、面倒臭いことこの上ないだろう。
NSDictionaryなら、大丈夫。それもできる。
ちなみに、NSArrayでも[]でのインデックスアクセスができる。

【コード例6】

int main(int argc, const char** argv)
{
    @autoreleasepool {
        NSMutableDictionary* dict = [NSMutableDictionary dictionary];
        
        // 添字アクセスによる要素の代入
        dict[@"key"] = @"val";
        
        // 添字アクセスによる要素の取得
        NSString* val = dict[@"key"];
        
        NSLog(@"%@", val);
    }
}

というわけで、数値や文字列をキーにできるときには、なるべくNSDictionaryを使ってあげるといいのかもしれない。

私は使わないけど。

*1:2014年4月17日追記:後から気づいたのですが、弱参照のゼロ化については、詳解Objective-C第3版のp125, 126に解説がありました。弱参照ポインタ変数は、ポインタの参照先オブジェクトが解放された際に、自動的にnilが代入されるそうです。