Objective-Cでmethod_missing

今日となりのチームの人と話してて、NSObjectにforwardInvocationというメソッドがある事を知りまして、ちゃんとObjective-Cも勉強して行こうという事で覚え書きです。

例えば以下のRubyのコードを実行すると、undefined methodエラーが発生します。

#!/usr/bin/ruby -Ku

class Dog
  def bark
    puts 'Bow-wow!'
  end
end

dog = Dog.new
dog.bark
dog.walk
$ ruby method_missing.rb
method_missing.rb:16: undefined method `walk' for #<Dog:0x102bb4448> (NoMethodError)
Bow-wow!

ここでmethod_missingをオーバーライドすると、このエラーをフック出来ます。

class Dog
  def bark
    puts 'Bow-wow!'
  end

  def method_missing(method_name, *args, &block)
    puts "this dog can't #{method_name}!"
  end
end
$ ruby method_missing.rb
Bow-wow!
this dog can't walk!

似た事はPythonでも可能で、__getattr__を利用して下記のような感じになります。(正確には違うけど)

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class Dog:
    def __getattr__(self, method_name):
        def method_missing():
            print "this dog can't %s!" % (method_name,)
        return method_missing

    def bark(self):
        print 'Bow-wow!'

dog = Dog()
dog.bark()
dog.walk()
$ python method_missing.py
Bow-wow!
this dog can't walk!

んで、Objective-Cでも同じ事できるよっていうのが、forwardInvocationです。RubyとかPythonに比べるとややこしいのですけど、オブジェクトが対応するメソッドが存在しないメッセージを受け取った際に、このforwardInvocationが呼ばれます。後はフォールバック用のメソッドを用意して、そちらに流すとmethod_missingと同じ挙動を実現出来ますね。methodSignatureForSelectorも合わせてオーバーライドしておかないと、forwardInvocationが呼ばれないので注意しましょう。

#import <Foundation/Foundation.h>

@interface Dog : NSObject
- (void)bark;
@end

@implementation Dog
- (void)bark {
	NSLog(@"Bow-wow!");
}

- (void)method_missing:(NSString*)sel {
    NSLog(@"this dog can't %@!", sel);
}

- (void)forwardInvocation:(NSInvocation *)invocation {
	NSLog(@"%s", __func__);
    [self method_missing:NSStringFromSelector([invocation selector])];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
	NSLog(@"%s", __func__);
    NSMethodSignature *sig = [super methodSignatureForSelector:sel];
    if (!sig) sig = [[self class] instanceMethodSignatureForSelector:@selector(method_missing:)];
    return sig;
}
@end


int main(int argc, char* argv) {
    NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

	Dog *dog = [[Dog alloc] init];
	[dog bark];
	[dog walk];
	
    [pool release];
    return 0;
}

ちなみにforwardInvocationはNSProxyにも定義されています。プロキシでメッセージのディスパッチ先を動的に変更するって話ですね。仕組み自体がコンパイルと相性が良くないですけど、上手く使うと面白いコードが書ける気がしなくもないです。

Leave a Reply

Your email address will not be published. Required fields are marked *