Method SwizzlingでNull挿入パターンを実装する

NSMutableArray、NSMutableDictionary等のCocoa標準のコレクションクラスは要素にnilを許さない。
さすがにそれでは不便だろうということでNSNullオブジェクトが用意されておりnilの代わりに格納することができるのだが、要素がnilかどうかチェックしてから格納するのは面倒だし、nilを無視するのがObjective-Cはずだったはずだ。

NSMutableDictionary(NSMutableArray)はnilを格納できない。
NSMutableDictionary* dic = [[NSMutableDictionary alloc] initWithCapacity:10];
if (aObject != nil) 
{
    [dic setObject:aObject forKey:@"key1"];
}
else
{
    [dic setObject:[NSNull null] forKey:@"key1"];
}

ということでこれをなんとかしたい。
NSNullを格納する構造を変えてしまうと色々な所で不具合が出ると思われるので、せめて要素のnilチェックをしなくても良いように、要素がnilだった場合は自動的にNSNullをセットするような実装にしたい。

ぱっと思いつく方法は

  • 継承クラスを用意してメソッドをオーバライドする
  • カテゴリを用意して別名のメソッドを追加する

どちらも正解だが、どうせならObjective-C 2.0ならではの方法で解決してみようと思う。

  • Method Swizzling

Method Swizzlingはすでに実装されているクラスのメソッドを自前のメソッドに入れ替えるObjective-Cならではの手法。実装を置換える方法といえばポージング(posing)が有名だが、MacOSX10.5 からはDeprecatedになっており、現在はこちらが主流である。

実装自体は簡単だ。置換えるためのメソッドをカテゴリなどに用意しておき、それを既定のメソッドと入れ替えてやるだけだ。※

例としてNSMutableDictionaryのsetObject:forKey:メソッドを置換えて、要素がnilであれば自動的にNSNullをセットする実装にしてみる。

NSMutableDictionary+NullCapability.m
- (id)initWithNullCapability:(NSUInteger)numItems
{
    self = [self initWithCapacity:numItems];
    SEL originalSelector = @selector(setObject:forKey:);
    SEL overrideSelector = @selector(swizz_setObject:forKey:);
   
    Class clazz = [self class];
    Method originalMethod = class_getInstanceMethod(clazz, originalSelector);
    Method overrideMethod = class_getInstanceMethod(clazz, overrideSelector);
    
    method_exchangeImplementations(originalMethod, overrideMethod);
    return self;
}
- (void)swizz_setObject:(id)anObject forKey:(id)aKey
{
    if (anObject)
    {
        [self swizz_setObject:anObject forKey:aKey];
    }
    else
    {
        [self swizz_setObject:[NSNull null] forKey:aKey];
    }
}

メソッドを置換えるタイミングはいろいろとあると思うが、ここではイニシャライザを使った。(これが良いかどうかは疑問だが)なので、使い方はこのカテゴリをインポートしておき、初期化にinitWithNullCapabilityを使うだけだ。

NSMutableDictionary* dic = [[NSMutableDictionary alloc]initWithNullCapability:10];
〜
[dic setObject:anObject forKey:"key1"]; //要素のnilチェックは不要

このsetObject:forKey:メソッドの呼び出しはswizz_setObject:forKeyを呼び出したこととなる。

swizz_setObjectメソッドはこの実装では再帰してしまわないのか? と思うのかもしれないが、そんなことは無い。同メソッドはランタイムには旧メソッド(setObject:forKey:)に置き換わっているので、同名のメソッドを呼ぶことで旧メソッドが実行される。(最初、私自身が勘違いして再帰ループを起こしたのは内緒だ)

※簡単と書いたがこれはObjective-Cだからであって、これをJava等で実現しようとするとDynamicProxyを介入させるか、AspectJAOPフレームワークを導入するか、Javassistのようなタイプエンハンサを導入してバイナリレベルで実装を書き換える等、大事になってしまう。