NSHashTableというクラスがある。
簡単に言うと、これはNSMutableSetと同じような振る舞いをしながらも、コンテンツに対して弱参照を持つことができるというコンテナだ。FoundationのコンテナクラスであるNSArray,NSSet,NSDictionaryは、原則としてコンテンツに対して強参照を持つ。これがどういうことかというと、あるコンテナに格納したオブジェクトのもとのオーナー(強参照を持っていた)オブジェクトが開放されても、コンテナ自体が開放されない限りコンテナ内のオブジェクトは開放されないということである。多くの場合はこれでいいのだが、都合の悪いこともある。
例えば、複数のデリゲートオブジェクトを保持する場合。原則としてデリゲートオブジェクトをプロパティやメンバで保持する場合は、weak属性を使う。何故かと言うと、通常デリゲートを使用する場合は、デリゲートプロトコルの実装者がデリゲートの呼び出し元をインスタンス化してその所有権を持つ(強参照を持つ)ことになる。モーダルViewControllerに対してのデリゲートなどがよくあるパターンだ。そして、デリゲートの呼び出し元(この例ではModalViewController)がデリゲート先(PresentingViewController)に対して強参照を持ってしまうと、 循環参照(Retain Cycle) が発生する。ARC環境では2つ以上のオブジェクトが相互に強参照を持つと、どちらも永遠に開放されずにメモリリークが起きる。なので、呼び出し元から呼び出し先への参照をweakにすることで、呼び出し元のretain countが0になった時(modalが閉じられた時)に適切に開放されることになる。
しかし、この場合だと任意の個数のデリゲートオブジェクトに対応できない。そういう場合はどうするか。まず思いつくのがNSArrayで管理する方法だ。だが先に述べたようにNSArrayはコンテンツに対して強参照を持ちながらオブジェクトを保持する。その場合、先の例とは逆のパターン、つまり、デリゲートの呼び出し元よりも先にデリゲートの呼び出し先が開放されるような場合に、間接的に循環参照が起きる。呼び出し元が保持しているNSArrayが呼び出し先を強参照しているため、実質的に呼び出し元が呼び出し先すべてを強参照していることになるからだ。
そのような場合、コンテナに対しては強参照(メンバなどで)を持っておきたいが、中身のオブジェクトに対しては弱参照を持ちたいと思うのが自然だ。NSHashTableはそんなときに使うクラスである。
詳しい使い方は
などを見るとよいと思う。
…そしてここからが本題。
以下のようなテストコードがある。このコードは一見パスしそうな空気を感じるが、一箇所だけ失敗するところがある。さて、どの行か?
- (void)testHashTable
{
NSHashTable *hashTable;
hashTable = [NSHashTable weakObjectsHashTable];
@autoreleasepool {
NSObject *weakobj1 = [NSObject new];
NSObject *weakobj2 = [NSObject new];
[hashTable addObject:weakobj1];
[hashTable addObject:weakobj2];
[hashTable addObject:self];
XCTAssert(hashTable.count == 3, @"コンテンツは3つのはず");
}
XCTAssert(hashTable.count == 1, @"@autoreleasepoolから抜けているのでweakobj1/2への参照が消えているはず");
XCTAssert(hashTable.allObjects.count == 1, @"〃");
XCTAssert([hashTable.anyObject isEqual:self], @"hashtableのコンテンツはselfのみのはず");
XCTAssert([hashTable.allObjects.firstObject isEqual:hashTable.allObjects.lastObject], @"〃");
for (id obj in hashTable.allObjects) {
XCTAssert([obj isEqual:self], @"hashtableのコンテンツはselfのみのはず");
}
for (id obj in hashTable) {
XCTAssert([obj isEqual:self], @"〃");
}
}
答えは以下。
( ゚д゚) ・・・
(つд⊂)ゴシゴシ
(;゚д゚) ・・・
(つд⊂)ゴシゴシゴシ
, .
(;゚ Д゚) …!?
まったく理由は分からないが、hashTable.count と hashTable.allObjects.countの値が違うのだ。ちなみに前者は3,後者は1である。この時点でweakobj1/2は解放済みなので、いわずもがな後者が正しい。ならば実際はまだweakobj1/2が開放されていないのかと疑うと、それも違う。以降のコードでhashTableが保持しているオブジェクトがselfだけだとわかる。ならば呼び出しの順番が悪いのか?と考えて問題の行を最後に回してみる。以下が結果。
~完~