讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 10.3 協議 >

10.3 協議

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 = NSObject
let tv = UITableView
tv.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 = MyClass
if 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已經知道了答案,所以後面就不會再對相同的對象進行同樣的檢查了。