performSelectorとパラメタ数

JavaC#のInvokeはそのオブジェクトの持つメタ情報からメソッド実行する(メッセージを送る)が、Objective-Cでそれに相当するのがNSObjectプロトコル(クラスではない)performSelector:メソッドだ。

メソッドの実態であるセレクタをメタ情報や文字列から生成して、メソッドに引き渡すパラメタと共にメッセージを送ることができる。
NSObject Protocol Reference

[self performSelector:aSelector];

[self performSelector:aSelector withObject:arg1];

[self performSelector:aSelector withObject:arg1 withObject:arg2];

ところでperformSelectorメソッドのオーパロードは上の三つであり、これ以上は無い。そう、3つ以上の引数が必要なセレクタにメッセージを送ることはできないのだ。※

これを回避する実装だが、当然ある。経験の少ない私が知っている手は二つ。

NSInvovationクラスを使う

NSProxyの所でも登場したNSInvocationクラス、セレクタから生成したNSMethodSignatureクラスを使うことでどんなメッセージでも送れる。

    NSMethodSignature* signature = [[self class] instanceMethodSignatureForSelector:aSelector];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    [invocation setSelector:selector];
    [invocation setTarget:self];

    //序数0はself、1は_cmd
    [invocation setArgument:arg1 atIndex:2];
    [invocation setArgument:arg2 atIndex:3];
    [invocation setArgument:arg3 atIndex:4];
    
    [invocation invoke];

setArgumentの序数を2からセットするのは、Objective-Cのメソッドの内部表現として序数0にはself、序数1には_cmd(セレクタ)が暗黙的にセットされるからだ。
この方法は少し冗長だが、Objective-Cらしい方法である。

もう一つはランタイムAPIのC関数を使う方法だ。

objc_msgSendランタイム関数を使う
    objc_msgSend(self, aSelector, arg1, arg2, arg3);

3つ目のパラメタ以降は可変長の引数なので、これならパラメタがいくつあっても大丈夫だ。
シンプルで高速だが、一つの間違いでアプリケーションは簡単にクラッシュするだろう。

これ以外の方法もあるかもしれないが、私には判らない。


※va_listを使うことでObjective-Cでも可変長引数は実装できるどうしてこのような作りになっているんだろう。引数の個数からしてCocoa(アクション・ターゲット)用かしら?


追記 2012/07/20

NSInvovationだがその後の検証の結果、可変個引数のシグネチャを持つメソッドには対応できないことが判った。
[iOS][Objective-C]NSInvocationは可変個引数のシグネチャに対応していない

残念だがこの場合の選択肢、特にパラメタ数が可変の場合はobjc_msgSendを使うしか無いようだ。