2011年1月31日月曜日

performSelectorで返り値がid型以外のメソッドを呼ぶ

返り値がid型以外のメソッドをperformSelectorで呼ばなければならないことがあるけど、performSelectorの返り値の型はidになっているため、floatや構造体などはキャストしてもコンパイルが通らない。また、コンパイルできたとしても返ってきた値をそのまま使うと問題になる場合もある。

performSelectorをリファレンスで調べると

For methods that return anything other than an object, use NSInvocation.

と書いてあるけど、NSInvocationを使うのはとても面倒だし、パフォーマンスも落ちるのでできるだけ使いたくない。

何かいい方法はないものかと調べてみると、comp.lang.objective-CのFAQにCan I use SEL for methods returning non-id types?という項目と、そのすぐ上にCan I use IMP for methods returning non-idtypes?という項目が見つかった。回答を短くまとめると、IMPを取り出してその関数ポインタを適切にキャストしてから呼び出せばいい、ということなのでやってみた。

NSNumber* number = [NSNumber numberWithFloat:M_PI];
NSLog(@"%f", [number floatValue]); // => 3.141593

float (*floatValueImp)(id, SEL) = (float(*)(id, SEL))[number methodForSelector:@selector(floatValue)];
NSLog(@"%f", floatValueImp(number, @selector(floatValue))); // => 3.141593

返り値が構造体でも問題ない。

NSValue* value = [NSValue valueWithRange:NSMakeRange(11, 13)];
NSLog(@"%@", NSStringFromRange([value rangeValue])); // => {11, 13}

NSRange (*rangeValueImp)(id, SEL) = (NSRange(*)(id, SEL))[value methodForSelector:@selector(rangeValue)];
NSLog(@"%@", NSStringFromRange(rangeValueImp(value, @selector(rangeValue)))); // => {11, 13}

ということで、関数ポインタのキャストが見にくいけど、NSInvocationを使うよりはいいんじゃないかと思った。

IMPを使うことですべて解決したわけだけど、そもそものきっかけは、

if ([a performSelector:@selector(methodThatReturnsBOOL:) withObject:b]) {
    ...
}

というように、BOOL値を返すメソッドをperformSelectorで呼んで、そのままif文の条件式に使っていたところ、ブロックがまったく実行されないという現象が起きたことだった。

NSLog(@"%d", [a performSelector:@selector(methodThatReturnsBOOL:) withObject:b])

のようにして値を表示してみると、セレクタが0を返していても-256と表示された。型のサイズが変わっているのが原因だと思うのだけど(idは構造体へのポインタ、BOOLsigned chartypedefされている)、特定の状況でしか起きなかったため発見するのに時間がかかってしまった。なお、BOOLの場合は返り値をキャストすることでも解決する。