Objective-C拥有协议,这相当于Swift的协议(参见第4章)。由于类是Objective-C唯一的对象类型,因此所有Objective-C协议都会被Swift看作类协议。与之相反,标记为@objc的Swift协议(隐式表示类协议)可以被Objective-C所看到。Cocoa大量使用了协议。
比如,下面来看看Cocoa对象是如何复制的。有些对象可以复制,有些则不行。这与对象的类继承没有关系。我们需要一个统一的方法,可复制的任何对象都会响应这个方法。因此,Cocoa定义了一个名为NSCopying的协议,它只声明了一个必要的方法copyWithZone:。下面是Objective-C中NSCopying协议的声明(在NSObject.h中):
@protocol NSCopying- (id)copyWithZone:(nullable NSZone *)zone;@end
转换为Swift如以下代码所示:
protocol NSCopying { func copyWithZone(zone: NSZone) -> AnyObject}
不过,NSObject.h中的NSCopying协议声明并没有表示NSObject遵循着NSCopying。实际上,NSObject并不遵循NSCopying!如下代码无法编译通过:
let obj = NSObject.copyWithZone(nil) // compile error
不过下面的代码可以编译通过,因为NSString遵循了NSCopying(字面值“howdy”会被隐式桥接为NSString):
let s = /"hello/".copyWithZone(nil)
典型的Cocoa模式是“只要实现了如下方法,那么任何类的实例都可以”。这显然是个协议。比如,考虑一下协议是如何与表视图(UITableView)产生联系的。表视图从数据源获取数据。出于这个目的,UITableView声明了一个dataSource属性,如下所示:
@property (nonatomic, weak, nullable) id <UITableViewDataSource> dataSource;
转换为Swift如以下代码所示:
weak var dataSource: UITableViewDataSource?
UITableViewDataSource是个协议。表视图说的是“我不管数据源属于哪个类,但不管哪个,它都应该遵循UITableViewDataSource协议”。这形成了一种承诺,数据源至少要实现必需的实例方法tableView:numberOfRowsInSection:与tableView:cellForRowAtIndexPath:,在需要知道显示什么数据时,表视图将会调用它们。在使用UITableView并且想要提供给它一个数据源对象时,该对象的类将会使用UITableViewDataSource,并实现所需的方法;否则,代码将无法编译通过:
let obj = NSObjectlet tv = UITableViewtv.dataSource = obj // compile error
毫无疑问,协议在Cocoa中最常使用的地方就是与委托模式有关了。第11章将会对此进行详尽的介绍,不过我们先来看看Empty Window项目中的一个示例:项目模板所提供的AppDelegate类的声明如下所示:
class AppDelegate: UIResponder, UIApplicationDelegate { // ...
AppDelegate的主要目的是作为共享的应用委托。共享的应用对象是一个UIApplication,而UIApplication的delegate属性的声明如下所示:
unowned(unsafe) var delegate: UIApplicationDelegate?
(第12章将会介绍unsafe修饰符。)UIApplicationDelegate类型是个协议。共享的UIApplication对象正是通过它知道其委托可以接收如application:didFinishLaunchingWithOptions:这样的消息。因此,AppDelegate类通过显式使用UIApplicationDelegate协议来表明其角色。
Cocoa协议拥有自己的文档页面。当UIApplication类文档告诉你delegate属性的类型为UIApplicationDelegate时,它实际上是隐式告诉你如果想要了解UIApplication的委托可以接收什么消息,那就需要查看UIApplicationDelegate协议文档。你在UIApplication类文档页面上找不到刚才提到的application:didFinishLaunchingWithOptions:!它的介绍位于UIApplicationDelegate协议的文档页面中。
当类使用了协议时,这种信息分离会让你感到困惑。当类文档上说类遵循了某个协议时,请不要忘记查看协议的文档!后者可能包含了关于类行为的重要信息。要想了解可以向某个对象发送什么消息,你需要沿着父类继承链向上查找;还需要查看该对象的类(或父类)所遵循的协议。比如,正如第8章所介绍的,只查看UIViewController类文档页面是不可能发现UIViewController有一个viewWillTransitionToSize:withTransitionCoordinator:事件的:你需要查看UIViewController所使用的协议UIContentContainer的文档。
10.3.1 非正式协议
你可能会在网上或文档中遇到非正式协议的说法。非正式协议实际上并不是协议;它只不过是向编译器提供了一个方法签名,这样编译器就允许发送消息而不会发出警告了。
有两种互补的方式可以实现非正式协议。一是在NSObject上定义一个类别;这样任何对象都可以接收类别中的消息了。二是定义一个协议,但却不必遵循它;相反,协议中的消息只会发送给类型为id(AnyObject)的对象,这样编译器就不会发出警告了。
这些技术在协议可以声明可选方法前得到了广泛的应用;但现在这么做就完全没必要了,而且这些技术还存在一定的风险。在iOS 9中,只有极少的非正式协议还得以留存。比如说,NSKeyValueCoding(本章后面将会介绍)是个非正式协议;你还会在文档和其他地方看到术语NSKeyValueCoding,不过实际上并没有该类型;它是NSObject上的一个类别。
10.3.2 可选方法
Objective-C协议以及标记为@objc的Swift协议可以拥有可选成员(参见4.8.4节)。问题在于:在实际开发中,可选方法是如何使用的?我们知道,如果向对象发送消息,但对象无法处理该消息,那就会抛出异常,应用有可能崩溃。不过方法声明是个契约,表示对象可以处理该消息。如果声明一个可能会,也可能不会实现的方法,那就破坏了契约,这么做是否会造成应用崩溃呢?
答案就是Objective-C是一门既动态又内省的语言。它可以询问对象是否能够处理消息,而无须实际发送消息。这里的关键方法是NSObject的respondsToSelector:,它接收一个选择器参数并返回Bool(选择器本质上是个方法名,不过其表示方式独立于任何方法调用;参见附录A)。因此,我们可以只在安全的情况下才向对象发送消息。
在Swift中演示respondsToSelector:有点棘手,因为让Swift抛弃严格的类型检查而允许我们向对象发送可能无法响应的消息是很困难的事情。在这个杜撰的示例中,我首先在顶层定义两个类:一个继承自NSObject,否则无法向其发送respondsToSelector:;另一个声明会根据条件发送的消息:
class MyClass : NSObject {}class MyOtherClass { @objc func woohoo {}}
现在可以这么做:
let mc = MyClassif mc.respondsToSelector(/"woohoo/") { (mc as AnyObject).woohoo}
注意到从mc到AnyObject的类型转换。这会导致Swift放弃其严格的类型检查;现在可以向该对象发送Swift知道的任何消息了,就好像Objective-C的内省机制一样,这正是将woohoo声明标记为@objc的原因所在。如你所知,Swift提供了一种简写来根据条件发送消息,即将一个问号放到消息名的后面:
let mc = MyClass(mc as AnyObject).woohoo?
在背后,这两种方式是完全一样的;后者是前者的语法糖。对于问号来说,Swift会调用respondsToSelector:,如果无法响应该选择器,那就不会向该对象发送woohoo消息。
这也说明了可选协议成员的工作方式。Swift对待可选协议成员的方式与AnyObject成员一样,这并非巧合。下面是第4章曾经介绍过的一个示例:
@objc protocol Flier { optional var song : String {get} optional func sing}
在类型为Flier的对象上调用sing?()时,背后会调用respondsToSelector:,用于确定这个调用是否是安全的。
你不应该随意发送消息,也不要在发送任何旧的消息前显式调用respondsToSelec-tor:,因为除了可选方法,这么做是毫无必要的,还会增加处理时间。不过Cocoa实际上会调用对象的respondsToSelector:。为了证实这一点,在Empty Window项目的AppDelegate中实现respondsToSelector:,并将日志打印出来:
override func respondsToSelector(aSelector: Selector) -> Bool { print(aSelector) return super.respondsToSelector(aSelector)}
当Empty Window应用启动后,我的电脑上的输出如下所示(省略了私有方法与对同一个方法多次调用的输出);
application:handleOpenURL:application:openURL:sourceApplication:annotation:application:openURL:options:applicationDidReceiveMemoryWarning:applicationWillTerminate:applicationSignificantTimeChange:application:willChangeStatusBarOrientation:duration:application:didChangeStatusBarOrientation:application:willChangeStatusBarFrame:application:didChangeStatusBarFrame:application:deviceAccelerated:application:deviceChangedOrientation:applicationDidBecomeActive:applicationWillResignActive:applicationDidEnterBackground:applicationWillEnterForeground:application:didResumeWithOptions:application:handleWatchKitExtensionRequest:reply:application:shouldSaveApplicationState:application:supportedInterfaceOrientationsForWindow:application:performFetchWithCompletionHandler:application:didReceiveRemoteNotification:fetchCompletionHandler:application:willFinishLaunchingWithOptions:application:didFinishLaunchingWithOptions:
Cocoa会检查哪个可选的UIApplicationDelegate协议方法(包括一些文档中没有提及的方法)被AppDelegate实例实现了,因为它是UIApplication对象的委托,遵循UIApplicationDelegate协议,它显式表明可以响应所有这些消息。整个委托模式(第11章将会介绍)都依赖于该项技术。注意到Cocoa所遵循的策略:当首次遇到目标对象时,它会检查所有的可选协议方法一次,并可能会将结果存储起来;这样,应用的速度会受到这个一次性的初始respondsToSelector:调用的影响,不过现在Cocoa已经知道了答案,所以后面就不会再对相同的对象进行同样的检查了。