通常,向一个对象发送它不能处理的消息会得到类似这样的错误提示:No visible @interface for 'SomeObject' declares the selector 'someSelector:'
或 instance method '-someMethod' not found
等等,而动态语言,如 Ruby,则有另一种处理方式:
class MyClass
def initialize
@items = []
end
def method_missing(m, *args, &block)
unless @items.respond_to? m
super(m, *args)
end
@items.__send__(m, *args)
end
end
t = MyClass.new
t.push "hello"
puts t.first # output: "hello"
t.trim # error: undefined method `trim' (NoMethodError)
这种特性赋予了开发者很大的自由性和便利性。
虽然 Objective-C 并不是动态语言,但却具备动态类型,有类似的特性 -- 消息转发。
快速转发
这种方式把消息原封不动地转发给目标对象,速度是最快的也是最简单的 -- 仅需在类里实现 - (id)forwardingTargetForSelector:(SEL)aSelector;
。
@interface NewArray : NSObject
@property (strong, nonatomic) NSArray *array;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (id)firstObject;
- (NSUInteger)count;
@end
@implementation NewArray
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [_array respondsToSelector:aSelector] ?
_array : nil;
}
- (id)firstObject {
return _array.count ? [_array objectAtIndex:0] : nil;
}
- (NSUInteger)count { return 0; }
@end
NewArray *arr = [[NewArray alloc] init];
arr.array = @[@"a", @"b"];
NSLog(@"%@ and %@, count %li", [(NSArray *)arr lastObject],
[arr firstObject], [arr count]);
// output: "b and a, count 0"
普通转发
这种方式让你可以控制目标对象、方法和参数,但是性能没有第一种方法好。这个方法需要实现 - (void)forwardInvocation:(NSInvocation *)anInvocation;
和 - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector;
。
@implementation NSArray (MessageForwarding)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
for (id obj in self) {
// Only apply for string that lenght == 1
if (1 == [(NSString *)obj length]) {
[anInvocation invokeWithTarget:obj];
}
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:sel];
if (!signature) {
for (id obj in self) {
if ((signature = [obj methodSignatureForSelector:aSelector])) {
break;
}
}
}
return signature;
}
@end
NSArray *arr = @[[NSMutableString stringWithString:@"a"],
[NSMutableString stringWithString:@"bc"]];
[(NSMutableString *)arr appendString:@"!"];
NSLog(@"%@", arr);
// output: "a! bc"
-methodSignatureForSelector:
的作用是告诉运行时参数有多少个、是什么类型,这是用来兼容 C 的。
转发的注意事项
可能你已经在上述代码里发现了一些问题,这是它们的共同问题:
- 发送非自己处理的消息时需要转换类型,如
[(NSArray *)arr lastObject]
; - 如果第一个接收消息的对象实现了同样的方法,则不会触发转发,如第一例中的
[arr count]
。
第一个问题,目的是让运行时知道要找什么类型,以第一例为例,如果改成 [(NSString *)arr lastObject]
则会触发 Xcode 报错,CodeRunner 则只会警告。一般来说,用目标类型即可,比如 NSArray
就用 NSArray
,不过我比较喜欢动态语言的方式,所以一般用 id
。
第二个问题,需要用别的方式来解决,NSProxy
。
NSProxy
使用 NSProxy
需要实现一个子类。
#import <Foundation/Foundation.h>
@interface ArrayProxy : NSProxy
@property (strong, nonatomic) NSArray *items;
+ (id)proxyWithTarget:(NSArray *)anArray;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
@end
@implementation ArrayProxy
+ (id)proxyWithTarget:(NSArray *)anArray {
ArrayProxy *proxy = [ArrayProxy alloc];
proxy.items = anArray;
return proxy;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
for (id obj in _items) {
[anInvocation invokeWithTarget:obj];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// DO NOT do this:
// NSMethodSignature *signature = [super methodSignatureForSelector:sel];
// Will raise `NSInvalidArgumentException`
NSMethodSignature *signature;
for (id obj in _items) {
if ((signature = [obj methodSignatureForSelector:aSelector])) {
break;
}
}
return signature;
}
@end
@implementation NSArray (MessageForwarding)
- (id)do { return [ArrayProxy proxyWithTarget:self]; }
@end
@implementation NSString (MessageForwarding)
- (NSUInteger)count {
NSLog(@"%li", self.length);
return self.length;
}
@end
NSArray *arr = @[@"a", @"bc"];
[[arr do] count];
// output: 1, 2
因为是经过 NSProxy
来处理,所以就不受对象是否能响应对应消息的限制。另,不要在 -methodSignatureForSelector:
里往 super
发送消息,会抛出 NSInvalidArgumentException
。
需要注意的是,-do
只返回最后一个结果:
NSLog(@"%li", [[arr do] length]); // 2
结尾
上面的示例代码仅供参考如何实现消息转发,除此以外的部分有更好的实现方式,比如 firstObject
就应该用 category
来实现,而有些需求,则可以使用 delegate
,比如 UI 元素。
我对上面三种消息转发的主要用法是扩展无法通过 category
实现的功能,比如为 NSMutableArray
添加可操作自身的方法,Objective-C 不允许操作自身,所以像 while(obj = arr.unshift())
之类的技巧就无法使用。