消息转发

笔者翻译自iOS Developer Library Message Forwarding

消息转发

向一个对象发送一个它不能处理的消息是错误的。然而,在宣布这个错误前,运行时系统将给这个接收者对象第二次处理消息的机会。

转发

如果你向一个对象发送一个它不能处理的消息,在宣布一个错误前,运行时系统将向该对象发送一条 forwardInvocation: 消息,并带有一个 NSInvocation 对象作为它的唯一参数——NSInvocation 对象封装了原始的消息和它传过来的参数。

你能实现 forwardInvocation: 方法向消息提供一个默认的响应,或者通过一些方式去避免发生错误。就像它的名字的字面意义一样,forwardInvocation: 通常被用作向另一个对象转发消息。

来看看转发的意图,想象接下来的情景:假设,起初,你正在设计一个能够响应 negotiate 消息的对象,并且你希望它的响应能包含另一类对象的响应。你能够很容易地实现它,通过在你实现的 negotiate 方法的代码某处将 negotiate 消息传给其他对象。

进一步考虑这种情况,猜想你希望你的对象对 negotiate 消息的响应仅仅成为在其他类中已经实现了的 negotiate 响应。 完成这个目标的一种方式是使你的类从其他类继承该方法。但是,这种方式不可能总能成功。这里可能对于“为什么你的类在继承层次结构上要和实现了 negotiate 方法的类处于不同的分支”有一个好的理由。

即使你的类不能继承 negotiate 方法,你仍然可以实现一个简单地传递消息到其他类的实例的方法,去“实现”它:

- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )
        return [someOtherObject negotiate];
    return self;
}

这种方式稍微有点笨重,尤其是如果你的对象有大量函数都需要向其他对象传递。你必须为每一个你想从其他类引入的方法实现一个新的方法去覆盖它。再者,如此实现将不可能处理你不知道的情况,在你写代码的时候,你已经确定了可能想要转发的的消息的全集。但这个集合可能依靠运行时的事件,它可能改变为在将来实现的新的方法和类。

第二种可能是通过 forwardInvocation: 消息,它对上面的问题提供了一个小的有组织性的解决方案,而且它是动态的而不是静态的。它的运行过程如下:当一个对象因为没有能够匹配消息中选择器的方法,而不能响应一个消息时,运行时系统会向该对象发送一个 forwardInvocation: 消息去通知它。每个对象都从 NSObject 类继承了 forwardInvocation: 方法。然而,NSObject 版本的方法只是简单地调用了 doesNotRecognizeSelector: 方法。通过重写 NSObject 的方法并按你自己的想法实现,你能利用 forwardInvocation: 方法把消息转发给其他对象。

为了转发一个消息,forwardInvocation: 方法需要做的事有:

  • 决定消息应该去哪
  • 把它和它起初的参数一起发送过去

消息能够通过 invokeWithTarget: 方法被发送:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
} 

被转发的消息的返回值会返回给起初的发送者。所有类型的返回值都能传给发送者,包括 id,结构体和双精度浮点数。

forwardInvocation: 方法扮演了不能识别消息的分发中心,将它们打包并发送给不同的接收者。或者它是一个传输站,发送所有的消息到同一个目标。它能把一个消息转换成另一个,或者仅仅“吃掉”一些消息,以便这里没有响应和错误。forwardInvocation: 能使多个消息都对应一个响应。forwardInvocation: 的功能如何取决它如何实现。然而,它提供的在转发链中链接对象的特性,打通了程序设计的可能。

提示:只有在接收者不能调用一个存在的方法时,forwardInvocation: 才能处理消息。比如:如果你希望你的对象转发 negotiate 消息到其他对象,它就不能有自己的 negotiate 方法。如果它有,消息将不能到达 forwardInvocation:

参考 Foundation 框架的 NSInvocation 类特性,获取更多有关转发和调用的信息。

转发和多继承

转发模仿继承,并且经常给 Objec-C 程序带来多继承的效果。如图所示,一个对象通过转发消息来响应它看起来就像是引入或继承了另一个类中已定义的方法。

消息转发_第1张图片
Forwarding.jpg

在例图中,一个 Warrior 类的实例向一个 Diplomat 类的实例转发了 negotiate 消息。Warrior 调用 negotiate 看起来就跟 Diplomat 调用一样。它看起来响应了 negotiate 消息,而且实际上,它确实响应了(虽然起作用的其实是 Diplomat 对象)。

转发消息的对象因此“继承“了来自继承层次结构的两个分支(它自己和响应消息的对象)的方法。在上面的例子中,看起来就像是 Warrior 类继承了 Diplomat 和它自己的父类。

转发提供了大部分你通常想通过多继承实现的特性。然而,它们之间也有着重要的区别:多继承将不同的的功能结合到一个单一的对象中,它趋向于单一的,多层次的对象。另一反面,转发将不同的责任分配给不同的对象,它分解问题为小对象,但是用这种方式把这些对象联系起来对消息发送者来说是透明的。

代理对象

转发不仅仅模仿多继承,它也使得开发能代表或“覆盖”更具实质性的对象的轻量级对象成为可能。代理对象代表着其他对象,并且把消息过滤给它们。

Object-C Programming Language 的"Remote Messaging"中讨论的代理(proxy)是这样一个代理对象。它关心把消息转发给远端接收者的管理信息,比如确保参数值通过连接被复制和被恢复等等。但它不尝试去做其他事,它不能复制远端对象的函数功能,而仅仅是给远端对象一个本地地址,通过该地址,它能收到在另一个应用中的消息。

其他类型的代理也是可能的。比如,假设你有一个操作大量数据的对象——或许它要创建一个复杂的图片,或者它要读一个在磁盘上的文件的内容。建立这个对象将会是耗时的,因此你更愿意懒惰地去完成它——当它确实被需要的时候或系统资源临时空闲的时候。但同时,你需要至少一个代表该对象的占位符,以便应用中的其他对象能够正常运行。

在这种情况下,你能够初始化创建它,不需要完全成型的对象,而仅仅为它使用一个轻量级的代理。这个对象能够独立完成一些事情,比如回答关于数据的问题,但是通常它持有一个大对象的地址,并且当时机成熟时,向大对象转发消息。当代理对象的 forwardInvocation: 方法收到一个指向其他对象的消息,它将确保对象是存在的,如果不存在将创建对象。所有大对象的消息都要通过代理。因此,就程序的其他部分而言,代理对象和大对象是一样的。

转发和继承

尽管转发模仿继承,但是 NSObject 类从没有搞混它们。类似 respondsToSelector:isKindOfClass: 的方法仅仅会在继承的层次结构上查找,不会通过转发链。例如:如果 Warrior 对象被询问是否能响应 negotiate 消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
...

结果为 NO,尽管它能收到 negotiate 消息,并且通过转发给 Diplomat,不会发生错误,也能响应它。

在多数情况下,NO 就是正确答案。但它可能会不是。如果你利用转发建立了一个代理对象或者扩展类的功能,转发机制在继承层次上是透明的。如果你想你的对象看起来像是真的继承了它转发的消息的行为,你需要重写 respondsToSelector:isKindOfClass: 方法去支持你的转发算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了 respondsToSelector:isKindOfClass:instancesRespondToSelector: 方法也应该反应到转发算法。如果协议被使用,conformsToProtocol: 方法同样应该被加入到修改列表中。类似的,如果一个对象转发它收到的任何远端消息,它应该被重写 methodSignatureForSelector: 方法以便能返回对最终能响应被转发消息的方法的精确描述。例如:如果一个对象能够向它的代理转发消息,你应该向下面一样实现 methodSignatureForSelector:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

你可以考虑将转发算法放到一个私有代码里,并且所有这些方法(包括 forwardInvocation:)将调用它。

提示:这是一项先进的技术,仅仅在遇到别无他法的时候才应该去使用。它的目的不是为了代替继承。如果你必须使用这项技术,请先确信你已经完全理解了转发类和接收转发的类的行为。

本节提到的方法在 Foundation 框架说明里的 NSObject 类特性中有详细描述。参考 Foundation 框架说明的 NSInvocation 类特性,了解更多有关 invokeWithTarget: 方法的信息。

你可能感兴趣的:(消息转发)