iOS9 with Swift 協議

協議

協議是一種表示類型的相通性的方法,往往這些類型某些方面迥異。比如,一個Bee對象和一個Bird對象就在飛行方面有共同之處。因此定義一個Flier類型就很有用。問題來了:在哪種意義上,Bee和Bird可以算Flier呢?

除了繼承,當然一個可能的選擇是類繼承的方式。如果Bee和Bird都是類,對于父類和子類是有繼承關系的。所以Flier可以使Bee和Bird的共同父類。然而這不一定是對的。因為Bee是一種昆蟲,而Bird不是。雖然他們都可以飛行。所以我們需要一個類,能把他們聯結在一起。

此外,如果Bee和Bird不是類怎么辦?要知道在Swift中,這是很可能發生的,很多重要的對象都可以由結構體來替代類。但是結構體卻沒有繼承等級。別忘了這是結構體和類的最主要的區別。所以說結構體也需要和類一樣能夠表達共同性。

Swift通過協議(Protocol)來解決這個問題。協議相當重要,在Swift Header中超多70個協議。此外Oc也有協議。Swift和Oc的協議大致相符,而且可以互換。Cocoa很依賴協議。

雖然協議是一種對象類型,但是卻沒有一個協議對象——你無法實例化一個協議。協議相當輕量化。一個協議的聲明只是一堆屬性和方法的列表。屬性沒有值,方法中沒有代碼。一個“真正的”對象就可以聲明它屬于這個協議;或者是說他采用(adopting)或者遵循(conforming)該協議。一個對象類型一旦采用了該協議就是表明它會使用協議列出的屬性和方法。

比如說,作為一個Flier需要包括一個fly方法。所以Flier協議就可以標明fly方法:在協議的列表中添加fly,并且使函數體為空,就像這樣:

1

任何類型,包括枚舉、結構體、類甚至其他的協議都可以采用這個協議。可以在協議前面用分號在聲明中連接到上面類型名后面。(如果采用者是子類,協議跟在它的父類之后,中間用逗號號隔開。)

若Bird是一個結構體,它可以這樣采用Filer協議:


2

出現了點問題,原來是沒有遵循協議內容使用fly方法,現在改一下:

3

以上就是協議的大致內容,不過在現實中,我們還需要協議中的方法充實一點。

Tips:在Swift2.0中,多虧了協議擴展,協議可以聲明方法并且提供具體實現內容,之后我將講解。

為什么要用協議?

你現在可以對于為什么要用協議摸不著頭腦。我們讓Bird采用了Flier,之后呢?如果我們想要Bird知道怎么去fly,為什么我們不直接給他一個fly方法,反而給他協議呢?原因和類型分不開。別忘了,協議也是一個類型。Flier也是。所以我可以在任何我能用類型的地方使用Flier—— 將它聲明為一個變量,例如用在函數參數處。


4

想想上面的代碼,它包含了協議的全部點。一個協議是一種類型,所以多態是成立的。協議提供了另一種表達類型和子類型概念的方式。由于可替換原則,這意味著Flier可以是任意類型的一個實例(注意定義的是實例方法,而非類方法)。無論對象類型是哪種,只要它采用了Flier協議,因為只要采用了該協議,就會有一個fly方法。因此編譯器就會允許我們向這個對象發送fly消息。這樣說了,一個Flier就是可以向其發送fly的對象。

然而反過來就不正確,也就是說有fly方法的對象不一定是Flier。畢竟只有采用了該協議才是Flier。


5

這個Bee雖然有fly方法,但他不是Flier,所以tellToFly是不會接受它作為其參數的。為了使代碼編譯,可以在其聲明后添加Flier協議:


6.1
6.2

現在可以說下實際應用了。就像我之前說的,Swift中有大量的協議,讓我們使用一二吧。其中一個很有用的協議就是CustomStringConvertible。這個協議需要我們使用description字符串屬性。這有一個神奇的效果:當該屬性的實例被用在字符\插入(Interpolation)或者輸出(Print)的時候,description屬性值將自動用于代表這個實例。

注意一個類型是可以采用多個協議的。比如Double類型就采用了CustomStringConvertible, Hashable, Comparable 等等協議。


7


當然上面的代碼是不會編譯的,因為我雖然讓他們采用了上述的協議,但是在結構體內部沒有定義具體的代碼。

協議類型的檢驗和轉型:

協議是一種類型,而且協議的采用者是其子類型。所以多態是適用的。因此當對象被聲明為協議類型時,在對象的聲明類型和實際類型之間調節的運算符就會工作。比如說,可以用is運算符去檢測該對象是Bee還是Bird,因為聲明類型Flier既可以是Bee又可以是Bird。


8

類似地,as!和as?可以被用來將聲明為協議類型的對象轉型為實際類型。這個能力相當重要,因為接受協議的對象常常需要傳遞消息,而這協議是不能完成的。比如getworm這個方法:


9

Bird作為Flier,它可以fly,而作為Bird,它可以getWorm。所以你不能告訴所有的Flier都讓他們去getWorm:


10

所以此時就需要將其進行轉型:


11

聲明一個協議:

協議只能在文件的最頂端聲明。通過使用關鍵字protocol來進行聲明,之后寫協議名(作為類型名,首字母大寫),之后再跟大括號,里面寫這些東西:

屬性(Properties):

在協議中,屬性聲明包括var關鍵字(不能是let)、屬性名、冒號、其類型和大括號(包含get或者get set)。在以前,采用者對于屬性的具體實施可以是可寫入的,而在現在,它必須是:作為只讀的計算屬性或者常量(let)存儲屬性,調用者不能使用get set 屬性。

至于聲明static或者class屬性,可以在屬性前加static關鍵字。如果是采用者是類,那么和在類屬性里面一樣自由。

方法(Method):

協議中的方法聲明就是將普通函數聲明去掉函數體,也就是,它沒有大括號和函數內容。任何對象函數類型都是合法的,包括init和subsccript。(下標也是沒有大括號和函數內容,但是和屬性一樣,會有get 或者get set)。

至于聲明static或者class屬性,可以在屬性前加static關鍵字。如果是采用者是類,那么和在類屬性里面一樣自由。

當在枚舉類型或者結構體類型中使用時,如果一個方法需要被聲明為mutating,那么這個協議必須標記為mutating;而且如果協議中沒有mutating,協議采用者不能添加mutating。但是,協議中有mutating,采用者可以省略。(其實這里說的都是要有mutating)

類型別名(Type Alias):

協議聲明中可以引入內部類型別名(Type alias)作為原本類型名的同義詞。比如,typealias Time = Double 允許Time類型在協議大括號內部進行引用。其他地方,比如采用者類型中,Time并不存在,但是Double是它的一個匹配。

協議采用(Protocol adoption):

協議自身就可以采用別的協議(一個或者多個),方法就像你所想的,分號后面用逗號隔開。事實上,這為你提供了建立完整的類型的二級繼承體系!這在Swift頭文件中被大量運用。

為了表達清楚,采用了別的協議的協議可能需要重復被采用協議的內容(在大括號里的),但是由于這種重復可能是隱式的,所以也不是必要的。然而,對象類型采用了這種協議既要滿足此協議中的內容,還要滿足該協議采用的其他協議的內容。

Tips:如果一個協議的目的僅僅是為了將其他協議組合起來(通過采用別的協議),而沒有添加任何的新要求,而且你只在一個地方用這個協議的話,你大可不必這樣。你可以直接用protocol<...,...>,括號里面的協議用逗號分隔。

可選的協議成員:

在Oc中,協議的成員可以被聲明為可選值,表示該成員不一定要在采用者中實現。為了更好地兼容Oc,Swift允許可選的協議成員,但是只限于顯式橋接到OC的協議中(通過將@objc置于協議之前),在這種協議中,可選的成員即內部的方法和屬性也要標記optional:


12

只有類才可以采用這樣的協議,而且該特性在該類是NSObject的子類的情況下才會工作,或者可選成員被@objc標記、

13

協議采用者并不保證會將可選成員實施,所以Swift不知道對其發送song或者sing消息是否安全。

在這種情況下,Swift通過將song包裝進可選值中來解決問題。如果Flier 的采用者沒用實現該屬性,那么其結果就是一個nil,顯然這沒有什么危害。


14

Tips:這是個很少見的情況,會以雙重包裝的可選值結束。比如,如果可選屬性的值是String?,那么取得的值就是String??。

Warning:該可選屬性可以通過其協議被聲明為{get set},但是沒有合法的語法在這種協議類型的對象中去為這種屬性賦值。如果f是一個Flier,song被定義為{get set},你就不能設置f.song,我覺得這是一個Swift的bug。

像sing這樣的可選方法,可能還要更復雜一些。如果方法沒有被實現,我們是被禁止首先調用它的。為了解決這個問題,該方法自己就自動被標記成了該類型的可選值。因此要向它發送消息,就必須先進行解包。比較安全的方法就是用?解包:


15

這段代碼就可以很安全地運行了。只要采用者實現了sing,那么效果就是向f發送sing。如果Flier采用者沒有實現sing,什么事也不會發生。如果用!強制解包,那么后面那種情況程序就會崩潰。

如果可選的方法返回一直值,那么此值也會被包在可選值中:


16

如果你調用sing?( ),結果返回的就是可選的字符串:


17

如果用!,那么結果就是String。

很多Cocoa協議都有可選成員。比如你的iOS app有一個app delegate類就采用了UIApplicationDelegate 協議。該協議的方法都是可選的。然而,事實上,這不會對你調用它們有什么影響,(你不需要對他們進行特別標注即@objc),因為你的appdelegate類已經是NSObject的子類了,所以這個特性將會直接生效,而不管你實不實現方法。同樣地,你常常會使UIViewController的子類采用有可選成員的Cocoa delegate協議,這也是NSObject的子類,所以你直接實現你想實現的方法就可以了,而不用特別的標記。

類的協議:

類的協議是指,在聲明中協議名后面冒號跟著class關鍵字的協議,表示它只能夠被 類 對象類型 采用:


18

(如果協議已經被@objc標記,那么就沒有必要標記class了,因為@objc屬性已經暗示它是一個類的協議了。)

一個主要的使用類的協議的原因就是:利用類的特性——特別的內存管理機制。

19

關鍵字weak標記了delegate屬性作為特殊內存管理,只有類的實例才有這種特殊內存管理。delegate屬性是協議類型,而結構體和枚舉都可能采用協議,所以為了滿足編譯器的要求:該對象事實上是類的實例,而不是結構體或者枚舉的實例,所以這個協議被聲明為類的協議。

隱式Required構造器:

設想一個協議聲明了一個構造器,而且一個類采用了這個協議。由于該協議的規定,該類和其子類必須實現該構造器。因此該類不僅要實現構造器,而且它必須標記其為required。所以說,定義在協議中的構造器是隱式required,該類被顯式強制實現這個要求。


20

編譯錯誤:Initializer requirement init( ) can only be satisfied by a required initializer in non-final class Bird.?

為了解決這個問題,我們必須指定我們的構造器為required:


21

或者,就像剛剛錯誤提示,還有一種方法,標記Bird類為final。這意味著它就不能有任何子類了——這也保證了這個問題不會再出現了。所以如果Bird標記為final,就不需要標記它的init為required了。

上面的代碼匯總,Bird并沒有被final標記,而是它的init被required標記。這意味著任何Bird 的子類將會實現制度構造器,所以就意味著它們放棄了構造器繼承,必須實現required構造器并且標記required。

上面提到的和現實中iOS編程一個令人煩惱的特性密不可分。比如,你subclass了內置的Cocoa類 UIViewController(你很可能這么做),然后你給了這個子類一個構造器(你很可能這么做):


22

這個代碼就不會編譯。編譯錯誤顯示:required initializer init(coder: ) must be provided by subclass of UIViewController.

我們現在就知道其中的緣故了:UIViewController采用了一個NSCoding 的協議,而這個協議需要一個構造器 init(coder: )。這都不是你造成的,UIViewController 和NSCoding都是Cocoa定義的。但是這不礙事,這就是我剛剛提到的情形。你的UIViewController子類要不就是繼承了init(coder:) 要不必須顯式實現它并且標記required。既然你的子類已經有了一個指定構造器,那么繼承就泡湯了,只能用后者了。

但是如果你壓根兒沒想過要實現這么個構造器,再這樣操作這就顯得沒有意義了。Xcode的Fix-It特性提供了下面的方法:


23

它既滿足了構造器(在c5,會有為什么即使他沒有完成條件,但依然是合法構造器的原因),又使得他在被調用的時候會自動crash。

如果你真的有要實現的東西,那么只需要刪除fatalError 一行,并且用你自己的代碼代替就可以了。最小的實現內容就是super.init(coder: aDecoder),當然如果你有需要初始化的屬性,你也可以先初始化它們。

不僅UIViewController,還有很多Cocoa的內置類都采用了NSCoding。如果你有它們的子類并且要自己初始化構造器,那么會經常遇到這個問題。

字面轉換:

Swift中的一個非常好的點就是:它的很多特性是在內部實現的而且在頭文件中可以看到。Literal就是一例。比如你可以直接寫出5來制造一個值為5的Int(而不需要很正式地寫Int(5))不是因為玄學,而是因為Int采用了IntegerLiteralConvertible協議。不只是Int Literal 是這樣工作,其他Literal也是這樣的原理。下面是在Swift頭文件中定義的literal convertible protocol:


24

你自己的對象類型也可采用上面的轉換協議。這意味著literal可以出現在你的對象類型實例可以出現的地方。

比如我們定義一個Nest類型,其中包括雞蛋個數變量:

25

因為Nest采用了IntegerLiteralConvertible,我們可以將Int傳至Nest可以用的地方,然后init(integerLiteral:)就會被自動調用,從而產生一個新的Nest對象包含特定的雞蛋數。


26
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容