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

4.8 協議

協議是一種表示不相關類型共性的方式。比如,Bee對象與Bird對象可能有一些共性,因為蜜蜂與鳥都能飛。因此,定義一個Flier類型會好一些;但問題在於:讓Bee與Bird都成為Fliers會有多大的意義呢?

當然了,一種可能是使用類繼承。如果Bee與Bird都是類,那就存在一種父類與子類的類繼承。這樣,Flier就是Bee與Bird的父類。問題在於,可能存在其他一些原因使得Flier不能作為Bee與Bird的父類。Bee是個Insect,而Bird不是;但它們都可以飛,這是彼此獨立的能力。我們需要一種類型可以某種方式透過類繼承體系,將不相關的類集成到一起。

此外,如果Bee與Bird都不是類該怎麼辦呢?在Swift中,這是非常有可能的。重要且強大的對象可以是結構體而非類,不過並不存在父結構體與子結構體的結構體層次體系。畢竟,這是結構體與類之間的一個主要差別。但結構體也需要像類一樣擁有和表達正常的共性特性。Bee結構體與Bird結構體怎麼可能都是Fliers呢?

Swift通過協議解決了這一問題。協議在Swift中是非常重要的;Swift頭文件中定義了70多個協議!此外,Objective-C也支持協議;Swift協議大體上與Objective-C協議一致,並且可以與之交換。Cocoa大量使用了協議。

協議是一種對像類型,不過並沒有協議對像——你無法實例化協議。協議要更加輕量級一些。協議聲明僅僅是一些屬性與方法列表而已。屬性沒有值,方法沒有代碼!其想法是「真實」的對象類型可以聲明它屬於某個協議類型;這叫作使用或遵循協議。使用協議的對象類型會遵守這樣一個契約:它會實現協議所列出的屬性與方法。

比如,假設成為Flier需要實現一個fly方法;那麼,Flier協議可以指定必須要有一個fly方法;為了做到這一點,它會列出fly方法,但卻沒有函數體,如下代碼所示:


protocol Flier {
    func fly
}
  

任何類型(枚舉、結構體、類,甚至是另一個協議)都可以使用該協議。為了做到這一點,它需要在聲明中的名字後面加上一個冒號,後跟協議名(如果使用者是個擁有父類的類,那麼父類後面還需要加上一個逗號,協議則位於該逗號後面)。

假設Bird是個結構體,那麼它可以像下面這樣使用Flier:


struct Bird : Flier {
} // compile error
  

目前來看一切都沒問題,不過上述代碼無法編譯通過。Bird結構體承諾要實現Flier協議的特性,現在它必須要履行承諾!fly方法是Flier協議的唯一要求。為了滿足這一點,我在Bird中增加了一個空的fly方法:


protocol Flier {
    func fly
}
struct Bird : Flier {
    func fly {
    }
}
  

這麼做就沒問題了!我們定義了一個協議,並且讓一個結構體使用該協議。當然了,在實際開發中,你可能希望使用者對協議方法的實現能夠做一些事情;不過,協議對此並沒有做任何規定。

在Swift 2.0中,協議可以聲明方法並提供實現,這要歸功於協議擴展,本章後面將會對此進行介紹。

4.8.1 為何使用協議

也許到這個時候你還不太理解協議到底有什麼用。我們讓Bird成為一個Flier,然後呢?如果想讓Bird知道如何飛,為什麼不在Bird中聲明一個fly方法,這樣就無須使用任何協議了。這個問題的答案與類型有關。別忘了,協議是一種類型;我們的協議Flier是一種類型。因此,我可以在需要類型的時候使用Flier。比如,可以用它聲明變量的類型,或函數參數的類型:


func tellToFly(f:Flier) {
    f.fly
}
  

仔細想想上面的代碼,因為它體現了協議的精髓。協議是一種類型,因此適用於多態。協議賦予我們表達類與子類概念的另一種方式。這意味著,根據替代法則,這裡的Flier可以是任何對像類型的實例:枚舉、結構體或類。對像類型是什麼不重要,只要它使用了Flier協議即可。如果使用了Flier協議,那麼它就會有fly方法,因為這是使用Flier協議所要求的!因此,編譯器允許我們向該對像發送fly消息。根據定義,Flier是個可以接收fly消息的對象。

不過,反過來就不行了;擁有fly方法的對象不一定就是Flier。它不一定遵循了協議的要求;對像類型必須要使用協議。如下代碼將無法編譯通過:


struct Bee {
    func fly {
    }
}
let b = Bee
tellToFly(b) // compile error
  

Bee可以接收fly消息,這是以Bee的身份做的。不過,tellToFly並不接收Bee參數;它接收的是Flier參數。形式上,Bee並非Flier。要想讓Bee成為Flier,只需形式上聲明Bee使用了Flier協議。如下代碼可以編譯通過:


struct Bee : Flier {
    func fly {
    }
}
let b = Bee
tellToFly(b)
  

關於鳥與蜜蜂的示例到此為止,下面來看看實際的示例吧!如前所述,Swift已經提供了大量的協議,下面讓我們自定義的類型使用其中一個協議。Swift提供的最有用的協議之一是CustomStringConvertible。CustomStringConvertible協議要求我們實現一個description String屬性。如果這麼做了,那就會有奇跡發生:在將該類型的實例用在字符串插入或print中時(或是控制台中的po命令),description屬性就會自動用來表示該實例。

回憶一下本章之前介紹的Filter枚舉,我向其中添加一個description屬性:


enum Filter : String {
    case Albums = "Albums"
    case Playlists = "Playlists"
    case Podcasts = "Podcasts"
    case Books = "Audiobooks"
    var description : String { return self.rawValue }
}
  

不過,這麼做還不足以讓Filter具備CustomStringConvertible協議的功能;要想做到這一點,我們還需要正式使用CustomStringConvertible協議。Filter聲明中已經有了一個冒號與類型,因此所使用的協議需要放在逗號後面:


enum Filter : String, CustomStringConvertible {
    case Albums = "Albums"
    case Playlists = "Playlists"
    case Podcasts = "Podcasts"
    case Books = "Audiobooks"
    var description : String { return self.rawValue }
}
  

現在,Filter已經正式使用CustomStringConvertible協議了。CustomStringConvertible協議要求我們實現一個description String屬性;我們已經實現了一個description String屬性,因此代碼可以編譯通過。現在可以向print傳遞一個Filter或將其插入到一個字符串中,其description將會被自動打印出來:


let type = Filter.Albums
print(type) // Albums
print("It is \(type)") // It is Albums
  

看到協議的強大威力了吧,你可以通過相同方式為任何對像類型賦予字符串轉換的能力。

一個類型可以使用多個協議!比如,內建的Double類型就使用了CustomStringConvertible、Hashable、Comparable和其他內建協議。要想聲明使用多個協議,請在聲明中將每個協議列在第一個協議後面,中間用逗號分隔。比如:


struct MyType : CustomStringConvertible, Hashable, Comparable {
    // ...
}
  

(當然,除非在MyType中聲明所需的方法,否則上述代碼將無法編譯通過;聲明完之後,MyType就真正使用了這些協議)。

4.8.2 協議類型測試與轉換

協議是一種類型,協議的使用者是其子類型,這裡使用了多態。因此,用於對像真實類型的那些運算符也可以用於聲明為協議類型的對象。比如,Flier協議被Bird與Bee使用了,那麼我們就可以通過is運算符測試某個Flier是否為Bird:


func isBird(f:Flier) -> Bool {
    return f is Bird
}
  

與之類似,as!與as?可用於將聲明為協議類型的對象向下轉換為其真正的類型。這是非常重要的,因為使用協議的對象可以接收協議無法接收的消息。比如,假設Bird有個getWorm方法:


struct Bird : Flier {
    func fly {
    }
    func getWorm {
    }
}
  

Bird能以Flier身份fly,但卻只能以Bird身份getWorm,你不能讓任意一個Flier去getWorm:


func tellGetWorm(f:Flier) {
    f.getWorm // compile error
}
  

不過,如果這個Flier是個Bird,那麼它顯然可以getWorm,這正是類型轉換要做的事情:


func tellGetWorm(f:Flier) {
    (f as? Bird)?.getWorm
}
  

4.8.3 聲明協議

只能在文件頂部聲明協議。要想聲明協議,請使用關鍵字protocol,後跟協議名;作為一種對像類型,協議名首字母應該是大寫的。接下來是一對花括號,裡面可以包含如下內容:

屬性

協議中的屬性聲明包含了var(不是let)、屬性名、冒號、類型,以及包含單詞get或get set的一對花括號。對於前者來說,使用者對該屬性的實現是可寫的;對於後者來說,它需要滿足如下規則:使用者不可以將get set屬性實現為只讀計算屬性或常量(let)存儲屬性。

要想聲明靜態/類屬性,請在前面加上關鍵字static。類使用者可以將其實現為類屬性。

方法

協議中的方法聲明是個沒有函數體的函數聲明,即沒有花括號,因此也沒有代碼。任何對像函數類型都是合法的,包括init與下標(在協議中聲明下標的語法與在對像類型中聲明下標的語法是相同的,只不過沒有函數體,就像協議中的屬性聲明一樣,它也可以包含get或get set)。

要想聲明靜態/類方法,請在前面加上關鍵字static。類使用者可以將其實現為類方法。

如果方法(由枚舉或結構體實現)想要聲明為mutating,那麼協議就必須指定mutating指令;如果協議沒有指定mutating,那麼使用者將無法添加。不過,如果協議指定了mutating,那麼使用者可以將其省略。

類型別名

協議可以通過聲明類型別名為聲明中的類型指定局部同義詞。比如,通過typealias Time=Double可以在協議花括號中使用Time類型;在其他地方(比如,使用協議的對象類型中)則不存在Time類型,不過可以使用Double類型。

在協議中還可以通過其他方式使用類型別名,稍後將會介紹。

協議使用

協議本身還可以使用一個或多個協議;語法與你想像的一樣,聲明中的協議名後面是一個冒號,後面跟著它所使用的協議列表,中間用逗號分隔。事實上,這種方式創建了一個二級類型層次!Swift頭文件中大量充斥了這種用法。

出於清晰的目的,使用了另一個協議的協議可以重複被使用的協議花括號中的內容,但不必這麼做,因為這種重複是隱式的。使用了這種協議的對象類型必須要滿足該協議以及該協議使用的所有協議的要求。

如果協議的唯一目的是將其他協議組合起來,但不會添加任何新功能,並且這種組合僅僅用在代碼中的一個地方,那麼可以通過即時創建組合協議以避免聲明協議。要想做到這一點,請使用類型名protocol<...,...>,其中尖括號中的內容是個逗號分隔的協議列表。

4.8.4 可選協議成員

在Objective-C中,協議成員可以聲明為optional,表示該成員不必被使用者實現,但也可以實現。為了與Objective-C保持兼容,Swift也支持可選協議成員,不過只用於顯式與Objective-C橋接的協議,方式是在聲明前加上@objc屬性。在這種協議中,可選成員(方法或屬性)是通過在聲明前加上optional關鍵字實現的:


@objc protocol Flier {
    optional var song : String {get}
    optional func sing
}
  

只有類可以使用這種協議,並且符合如下兩種情況之一才能使用該特性:類是NSObject子類,或者可選成員被標記為@objc特性:


class Bird : Flier {
    @objc func sing {
        print("tweet")
    }
}
  

可選成員不保證會被使用者實現,因此Swift並不知曉向Flier發送song消息或sing消息是否安全。

對於song這樣的可選屬性來說,Swift通過將其值包裝到Optional中來解決這個問題。如果Flier使用者沒有實現該屬性,那麼結果就是nil,並不會出現什麼問題:


let f : Flier = Bird
let s = f.song // s is an Optional wrapping a String   

這是很少會出現的要使用雙重Optional的一種情況。比如,如果可選屬性song的值是個String?,那麼從Flier中獲取其值就會得到一個String??。

可選屬性可以由協議聲明為{get set},不過並沒有相關的語法可以設置該協議類型對像中的這種屬性。比如,如果f是個Flier,其song被聲明為{get set},那麼你就不能設置f.song。我認為這是語言的一個Bug。

對於像sing這樣的可選方法來說,事情將變得更為複雜。如果方法沒有實現,那麼我們就不可以調用它。為了解決這一問題,方法本身會被自動變成其所聲明類型的Optional版本。因此,要想向Flier發送sing消息,你需要將其展開。安全的做法是以可選的方式展開它,使用一個問號:


let f : Flier = Bird
f.sing?
  

上述代碼可以編譯通過,也可以安全地運行。效果相當於只有當f實現了sing時才向其發送sing消息。如果使用者的實際類型並未實現sing,那麼什麼都不會發生。雖然可以強制展開調用(f.sing!()),不過如果使用者沒有實現sing,那麼應用將會崩潰。

如果可選方法返回一個值,那麼它也會被包裝到Optional中。比如:


@objc protocol Flier {
    optional var song : String {get}
    optional func sing -> String
}
  

如果現在在Flier上調用sing?(),那麼結果就是一個包裝了String的Optional:


let f : Flier = Bird
let s = f.sing? // s is an Optional wrapping a String
  

如果強制展開調用(sing!()),那麼結果要麼是一個String(如果使用者實現了sing),要麼應用崩潰(如果使用者沒有實現sing)。

很多Cocoa協議都有可選成員。比如,iOS應用會有一個應用委託類,它使用了UIApplicationDelegate協議;該協議有很多方法,所有方法都是可選的。不過,這對如何實現這些方法是沒有影響的;你無須通過任何特殊的方式標記它們。應用委託類已經是NSObject的子類,因此該特性可以正常使用,無論是否實現了方法都如此。與之類似,你常常會讓UIViewController子類使用帶有可選成員的Cocoa委託協議;它也是NSObject的子類,因此你只需實現想要實現的那些方法,不必做任何特殊的標記。(第10章將會深入介紹Cocoa協議,第11章則會深入介紹委託協議。)

4.8.5 類協議

名字後面的冒號後使用關鍵字class聲明的協議是類協議,表示該協議只能由類對像類型使用:


protocol SecondViewControllerDelegate : class {
    func acceptData(data:AnyObject!)
}
  

(如果協議已經被標記為@objc,那就無須使用class;@objc特性隱含表示這還是個類協議。)

聲明類協議的典型目的在於利用專屬於類的內存管理特性。目前還沒有介紹過內存管理,不過還是先給出示例吧(第5章介紹內存管理時還會探討這個主題)。


class SecondViewController : UIViewController {
    weak var delegate : SecondViewControllerDelegate?
    // ...
}
  

關鍵字weak標識delegate屬性將會使用特殊的內存管理,只有類實例可以使用這種特殊的內存管理。delegate屬性的類型是個協議,而協議可以由結構體或枚舉類型使用。為了告訴編譯器該對像實際上是個類實例而非結構體或枚舉實例,這裡的協議被聲明成了類協議。

4.8.6 隱式必備初始化器

假設協議聲明了一個初始化器,同時一個類使用了該協議。根據協議的約定,該類及其子類必須要實現這個初始化器。因此,該類不僅要實現該初始化器,還要將其標記為required。這樣,聲明在協議中的初始化器就是隱式必備的,而類則需要顯式滿足這個要求。

下面這個簡單的示例是無法通過編譯的:


protocol Flier {
    init
}
class Bird : Flier {
    init {} // compile error
}
  

上述代碼會產生一段詳細且信息豐富的編譯錯誤消息:「Initializer requirement init()can only be satisfied by a required initializer in non-final class Bird.」要想讓代碼編譯通過,我們需要將初始化器指定為required。


protocol Flier {
    init
}
class Bird : Flier {
    required init {}
}
  

正如編譯錯誤消息所示,我們可以將Bird類標記為final。這意味著它不能有任何子類,從而確保這個問題不會再出現。如果將Bird標記為final,那就沒必要將init標記為required了。

在上述代碼中,我們並未將Bird標記為final,但其init被標記為了required。如前所述,這意味著如果Bird實現了指定初始化器(從而喪失了初始化器的繼承),那麼其子類就必須要實現必備初始化器,並將其標記為required。

該解決方案用於處理本章之前提到的Swift iOS編程中一個奇怪、惱人的特性。假設繼承了內建的Cocoa類UIViewController(很多時候你都會這麼做),並且為子類添加了一個初始化器(很多時候你也會這麼做):


class ViewController: UIViewController {
    init {
        super.init(nibName: "ViewController", bundle: nil)
    }
}
  

上述代碼無法編譯通過,編譯器會報錯:「required initializer init(coder:)must be provided by subclass of UIViewController.」

我們需要理解所發生的事情。UIViewController使用了協議NSCoding。該協議需要一個初始化器init(coder:)。不過,這些都不是你做的;UIViewController與NSCoding是由Cocoa而不是你聲明的。但這都沒關係!這與上述情況一樣。你的UIViewController子類要麼繼承init(coder:),要麼顯式實現它並將其標記為required。由於子類已經實現了自己的指定初始化器(從而喪失了初始化器繼承),因此它還需要實現init(coder:)並將其標記為required!

不過,如果不希望在UIViewController子類中調用init(coder:),這樣做就沒什麼用了。這麼做只不過是提供了一個沒什麼用處的初始化器而已。幸好,Xcode的Fix-It特性會幫助你生成這個初始化器,如下代碼所示:


required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
  

上述代碼符合編譯器的要求。(第5章將會介紹為什麼說它不符合初始化器的契約,但還是一個合法的初始化器。)如果調用這個初始化器,那麼程序就會崩潰,這是有意而為之的。

如果希望這個初始化器完成一些功能,那麼請刪除fatalError這一行,然後插入自己的功能實現代碼。一個有意義且代碼量最小的實現是super.init(coder:aDecoder);當然,如果類有需要初始化的屬性,那就需要先初始化它們。

除了UIViewController,還有很多內建的Cocoa類都使用了NSCoding。在繼承這些類並實現自己的初始化器時就會遇到這個問題,你得習慣才行。

4.8.7 字面值轉換

Swift的精妙之處在於,相對於內建以及魔法實現,它的很多特性都是由Swift本身實現的,並且可以通過Swift頭文件一探究竟,字面值就是這樣的。相對於通過Int(5)來初始化一個Int,你可以直接將5賦給它,其原因並不是來自於什麼神奇魔法,而是因為Int使用了協議IntegerLiteralConvertible。除了Int字面值,所有字面值均如此。如下字面值轉換協議都聲明在Swift頭文件中:

·NilLiteralConvertible

·BooleanLiteralConvertible

·IntegerLiteralConvertible

·FloatLiteralConvertible

·StringLiteralConvertible

·ExtendedGraphemeClusterLiteralConvertible

·UnicodeScalarLiteralConvertible

·ArrayLiteralConvertible

·DictionaryLiteralConvertible

你自己定義的對象類型也可以使用字面值轉換協議,這意味著可以在需要對像類型實例的情況下使用字面值!比如,下面聲明了一個Nest類型,它包含了一些雞蛋(即eggCount):


struct Nest : IntegerLiteralConvertible {
    var eggCount : Int = 0
    init {}
    init(integerLiteral val: Int) {
        self.eggCount = val
    }
}
  

由於Nest使用了IntegerLiteralConvertible,我們可以在需要Nest的地方使用Int,init(integerLiteral:)會自動調用,這會創建一個具有指定eggCount的全新Nest對像:


func reportEggs(nest:Nest) {
    print("this nest contains \(nest.eggCount) eggs")
}
reportEggs(4) // this nest contains 4 eggs