協(xié)議
協(xié)議是一種表示類型的相通性的方法,往往這些類型某些方面迥異。比如,一個(gè)Bee對象和一個(gè)Bird對象就在飛行方面有共同之處。因此定義一個(gè)Flier類型就很有用。問題來了:在哪種意義上,Bee和Bird可以算Flier呢?
除了繼承,當(dāng)然一個(gè)可能的選擇是類繼承的方式。如果Bee和Bird都是類,對于父類和子類是有繼承關(guān)系的。所以Flier可以使Bee和Bird的共同父類。然而這不一定是對的。因?yàn)锽ee是一種昆蟲,而Bird不是。雖然他們都可以飛行。所以我們需要一個(gè)類,能把他們聯(lián)結(jié)在一起。
此外,如果Bee和Bird不是類怎么辦?要知道在Swift中,這是很可能發(fā)生的,很多重要的對象都可以由結(jié)構(gòu)體來替代類。但是結(jié)構(gòu)體卻沒有繼承等級。別忘了這是結(jié)構(gòu)體和類的最主要的區(qū)別。所以說結(jié)構(gòu)體也需要和類一樣能夠表達(dá)共同性。
Swift通過協(xié)議(Protocol)來解決這個(gè)問題。協(xié)議相當(dāng)重要,在Swift Header中超多70個(gè)協(xié)議。此外Oc也有協(xié)議。Swift和Oc的協(xié)議大致相符,而且可以互換。Cocoa很依賴協(xié)議。
雖然協(xié)議是一種對象類型,但是卻沒有一個(gè)協(xié)議對象——你無法實(shí)例化一個(gè)協(xié)議。協(xié)議相當(dāng)輕量化。一個(gè)協(xié)議的聲明只是一堆屬性和方法的列表。屬性沒有值,方法中沒有代碼。一個(gè)“真正的”對象就可以聲明它屬于這個(gè)協(xié)議;或者是說他采用(adopting)或者遵循(conforming)該協(xié)議。一個(gè)對象類型一旦采用了該協(xié)議就是表明它會使用協(xié)議列出的屬性和方法。
比如說,作為一個(gè)Flier需要包括一個(gè)fly方法。所以Flier協(xié)議就可以標(biāo)明fly方法:在協(xié)議的列表中添加fly,并且使函數(shù)體為空,就像這樣:
任何類型,包括枚舉、結(jié)構(gòu)體、類甚至其他的協(xié)議都可以采用這個(gè)協(xié)議。可以在協(xié)議前面用分號在聲明中連接到上面類型名后面。(如果采用者是子類,協(xié)議跟在它的父類之后,中間用逗號號隔開。)
若Bird是一個(gè)結(jié)構(gòu)體,它可以這樣采用Filer協(xié)議:
出現(xiàn)了點(diǎn)問題,原來是沒有遵循協(xié)議內(nèi)容使用fly方法,現(xiàn)在改一下:
以上就是協(xié)議的大致內(nèi)容,不過在現(xiàn)實(shí)中,我們還需要協(xié)議中的方法充實(shí)一點(diǎn)。
Tips:在Swift2.0中,多虧了協(xié)議擴(kuò)展,協(xié)議可以聲明方法并且提供具體實(shí)現(xiàn)內(nèi)容,之后我將講解。
為什么要用協(xié)議?
你現(xiàn)在可以對于為什么要用協(xié)議摸不著頭腦。我們讓Bird采用了Flier,之后呢?如果我們想要Bird知道怎么去fly,為什么我們不直接給他一個(gè)fly方法,反而給他協(xié)議呢?原因和類型分不開。別忘了,協(xié)議也是一個(gè)類型。Flier也是。所以我可以在任何我能用類型的地方使用Flier—— 將它聲明為一個(gè)變量,例如用在函數(shù)參數(shù)處。
想想上面的代碼,它包含了協(xié)議的全部點(diǎn)。一個(gè)協(xié)議是一種類型,所以多態(tài)是成立的。協(xié)議提供了另一種表達(dá)類型和子類型概念的方式。由于可替換原則,這意味著Flier可以是任意類型的一個(gè)實(shí)例(注意定義的是實(shí)例方法,而非類方法)。無論對象類型是哪種,只要它采用了Flier協(xié)議,因?yàn)橹灰捎昧嗽搮f(xié)議,就會有一個(gè)fly方法。因此編譯器就會允許我們向這個(gè)對象發(fā)送fly消息。這樣說了,一個(gè)Flier就是可以向其發(fā)送fly的對象。
然而反過來就不正確,也就是說有fly方法的對象不一定是Flier。畢竟只有采用了該協(xié)議才是Flier。
這個(gè)Bee雖然有fly方法,但他不是Flier,所以tellToFly是不會接受它作為其參數(shù)的。為了使代碼編譯,可以在其聲明后添加Flier協(xié)議:
現(xiàn)在可以說下實(shí)際應(yīng)用了。就像我之前說的,Swift中有大量的協(xié)議,讓我們使用一二吧。其中一個(gè)很有用的協(xié)議就是CustomStringConvertible。這個(gè)協(xié)議需要我們使用description字符串屬性。這有一個(gè)神奇的效果:當(dāng)該屬性的實(shí)例被用在字符\插入(Interpolation)或者輸出(Print)的時(shí)候,description屬性值將自動用于代表這個(gè)實(shí)例。
注意一個(gè)類型是可以采用多個(gè)協(xié)議的。比如Double類型就采用了CustomStringConvertible, Hashable, Comparable 等等協(xié)議。
當(dāng)然上面的代碼是不會編譯的,因?yàn)槲译m然讓他們采用了上述的協(xié)議,但是在結(jié)構(gòu)體內(nèi)部沒有定義具體的代碼。
協(xié)議類型的檢驗(yàn)和轉(zhuǎn)型:
協(xié)議是一種類型,而且協(xié)議的采用者是其子類型。所以多態(tài)是適用的。因此當(dāng)對象被聲明為協(xié)議類型時(shí),在對象的聲明類型和實(shí)際類型之間調(diào)節(jié)的運(yùn)算符就會工作。比如說,可以用is運(yùn)算符去檢測該對象是Bee還是Bird,因?yàn)槁暶黝愋虵lier既可以是Bee又可以是Bird。
類似地,as!和as?可以被用來將聲明為協(xié)議類型的對象轉(zhuǎn)型為實(shí)際類型。這個(gè)能力相當(dāng)重要,因?yàn)榻邮軈f(xié)議的對象常常需要傳遞消息,而這協(xié)議是不能完成的。比如getworm這個(gè)方法:
Bird作為Flier,它可以fly,而作為Bird,它可以getWorm。所以你不能告訴所有的Flier都讓他們?nèi)etWorm:
所以此時(shí)就需要將其進(jìn)行轉(zhuǎn)型:
聲明一個(gè)協(xié)議:
協(xié)議只能在文件的最頂端聲明。通過使用關(guān)鍵字protocol來進(jìn)行聲明,之后寫協(xié)議名(作為類型名,首字母大寫),之后再跟大括號,里面寫這些東西:
屬性(Properties):
在協(xié)議中,屬性聲明包括var關(guān)鍵字(不能是let)、屬性名、冒號、其類型和大括號(包含get或者get set)。在以前,采用者對于屬性的具體實(shí)施可以是可寫入的,而在現(xiàn)在,它必須是:作為只讀的計(jì)算屬性或者常量(let)存儲屬性,調(diào)用者不能使用get set 屬性。
至于聲明static或者class屬性,可以在屬性前加static關(guān)鍵字。如果是采用者是類,那么和在類屬性里面一樣自由。
方法(Method):
協(xié)議中的方法聲明就是將普通函數(shù)聲明去掉函數(shù)體,也就是,它沒有大括號和函數(shù)內(nèi)容。任何對象函數(shù)類型都是合法的,包括init和subsccript。(下標(biāo)也是沒有大括號和函數(shù)內(nèi)容,但是和屬性一樣,會有g(shù)et 或者get set)。
至于聲明static或者class屬性,可以在屬性前加static關(guān)鍵字。如果是采用者是類,那么和在類屬性里面一樣自由。
當(dāng)在枚舉類型或者結(jié)構(gòu)體類型中使用時(shí),如果一個(gè)方法需要被聲明為mutating,那么這個(gè)協(xié)議必須標(biāo)記為mutating;而且如果協(xié)議中沒有mutating,協(xié)議采用者不能添加mutating。但是,協(xié)議中有mutating,采用者可以省略。(其實(shí)這里說的都是要有mutating)
類型別名(Type Alias):
協(xié)議聲明中可以引入內(nèi)部類型別名(Type alias)作為原本類型名的同義詞。比如,typealias Time = Double 允許Time類型在協(xié)議大括號內(nèi)部進(jìn)行引用。其他地方,比如采用者類型中,Time并不存在,但是Double是它的一個(gè)匹配。
協(xié)議采用(Protocol adoption):
協(xié)議自身就可以采用別的協(xié)議(一個(gè)或者多個(gè)),方法就像你所想的,分號后面用逗號隔開。事實(shí)上,這為你提供了建立完整的類型的二級繼承體系!這在Swift頭文件中被大量運(yùn)用。
為了表達(dá)清楚,采用了別的協(xié)議的協(xié)議可能需要重復(fù)被采用協(xié)議的內(nèi)容(在大括號里的),但是由于這種重復(fù)可能是隱式的,所以也不是必要的。然而,對象類型采用了這種協(xié)議既要滿足此協(xié)議中的內(nèi)容,還要滿足該協(xié)議采用的其他協(xié)議的內(nèi)容。
Tips:如果一個(gè)協(xié)議的目的僅僅是為了將其他協(xié)議組合起來(通過采用別的協(xié)議),而沒有添加任何的新要求,而且你只在一個(gè)地方用這個(gè)協(xié)議的話,你大可不必這樣。你可以直接用protocol<...,...>,括號里面的協(xié)議用逗號分隔。
可選的協(xié)議成員:
在Oc中,協(xié)議的成員可以被聲明為可選值,表示該成員不一定要在采用者中實(shí)現(xiàn)。為了更好地兼容Oc,Swift允許可選的協(xié)議成員,但是只限于顯式橋接到OC的協(xié)議中(通過將@objc置于協(xié)議之前),在這種協(xié)議中,可選的成員即內(nèi)部的方法和屬性也要標(biāo)記optional:
只有類才可以采用這樣的協(xié)議,而且該特性在該類是NSObject的子類的情況下才會工作,或者可選成員被@objc標(biāo)記、
協(xié)議采用者并不保證會將可選成員實(shí)施,所以Swift不知道對其發(fā)送song或者sing消息是否安全。
在這種情況下,Swift通過將song包裝進(jìn)可選值中來解決問題。如果Flier 的采用者沒用實(shí)現(xiàn)該屬性,那么其結(jié)果就是一個(gè)nil,顯然這沒有什么危害。
Tips:這是個(gè)很少見的情況,會以雙重包裝的可選值結(jié)束。比如,如果可選屬性的值是String?,那么取得的值就是String??。
Warning:該可選屬性可以通過其協(xié)議被聲明為{get set},但是沒有合法的語法在這種協(xié)議類型的對象中去為這種屬性賦值。如果f是一個(gè)Flier,song被定義為{get set},你就不能設(shè)置f.song,我覺得這是一個(gè)Swift的bug。
像sing這樣的可選方法,可能還要更復(fù)雜一些。如果方法沒有被實(shí)現(xiàn),我們是被禁止首先調(diào)用它的。為了解決這個(gè)問題,該方法自己就自動被標(biāo)記成了該類型的可選值。因此要向它發(fā)送消息,就必須先進(jìn)行解包。比較安全的方法就是用?解包:
這段代碼就可以很安全地運(yùn)行了。只要采用者實(shí)現(xiàn)了sing,那么效果就是向f發(fā)送sing。如果Flier采用者沒有實(shí)現(xiàn)sing,什么事也不會發(fā)生。如果用!強(qiáng)制解包,那么后面那種情況程序就會崩潰。
如果可選的方法返回一直值,那么此值也會被包在可選值中:
如果你調(diào)用sing?( ),結(jié)果返回的就是可選的字符串:
如果用!,那么結(jié)果就是String。
很多Cocoa協(xié)議都有可選成員。比如你的iOS app有一個(gè)app delegate類就采用了UIApplicationDelegate 協(xié)議。該協(xié)議的方法都是可選的。然而,事實(shí)上,這不會對你調(diào)用它們有什么影響,(你不需要對他們進(jìn)行特別標(biāo)注即@objc),因?yàn)槟愕腶ppdelegate類已經(jīng)是NSObject的子類了,所以這個(gè)特性將會直接生效,而不管你實(shí)不實(shí)現(xiàn)方法。同樣地,你常常會使UIViewController的子類采用有可選成員的Cocoa delegate協(xié)議,這也是NSObject的子類,所以你直接實(shí)現(xiàn)你想實(shí)現(xiàn)的方法就可以了,而不用特別的標(biāo)記。
類的協(xié)議:
類的協(xié)議是指,在聲明中協(xié)議名后面冒號跟著class關(guān)鍵字的協(xié)議,表示它只能夠被 類 對象類型 采用:
(如果協(xié)議已經(jīng)被@objc標(biāo)記,那么就沒有必要標(biāo)記class了,因?yàn)锧objc屬性已經(jīng)暗示它是一個(gè)類的協(xié)議了。)
一個(gè)主要的使用類的協(xié)議的原因就是:利用類的特性——特別的內(nèi)存管理機(jī)制。
關(guān)鍵字weak標(biāo)記了delegate屬性作為特殊內(nèi)存管理,只有類的實(shí)例才有這種特殊內(nèi)存管理。delegate屬性是協(xié)議類型,而結(jié)構(gòu)體和枚舉都可能采用協(xié)議,所以為了滿足編譯器的要求:該對象事實(shí)上是類的實(shí)例,而不是結(jié)構(gòu)體或者枚舉的實(shí)例,所以這個(gè)協(xié)議被聲明為類的協(xié)議。
隱式Required構(gòu)造器:
設(shè)想一個(gè)協(xié)議聲明了一個(gè)構(gòu)造器,而且一個(gè)類采用了這個(gè)協(xié)議。由于該協(xié)議的規(guī)定,該類和其子類必須實(shí)現(xiàn)該構(gòu)造器。因此該類不僅要實(shí)現(xiàn)構(gòu)造器,而且它必須標(biāo)記其為required。所以說,定義在協(xié)議中的構(gòu)造器是隱式required,該類被顯式強(qiáng)制實(shí)現(xiàn)這個(gè)要求。
編譯錯(cuò)誤:Initializer requirement init( ) can only be satisfied by a required initializer in non-final class Bird.?
為了解決這個(gè)問題,我們必須指定我們的構(gòu)造器為required:
或者,就像剛剛錯(cuò)誤提示,還有一種方法,標(biāo)記Bird類為final。這意味著它就不能有任何子類了——這也保證了這個(gè)問題不會再出現(xiàn)了。所以如果Bird標(biāo)記為final,就不需要標(biāo)記它的init為required了。
上面的代碼匯總,Bird并沒有被final標(biāo)記,而是它的init被required標(biāo)記。這意味著任何Bird 的子類將會實(shí)現(xiàn)制度構(gòu)造器,所以就意味著它們放棄了構(gòu)造器繼承,必須實(shí)現(xiàn)required構(gòu)造器并且標(biāo)記required。
上面提到的和現(xiàn)實(shí)中iOS編程一個(gè)令人煩惱的特性密不可分。比如,你subclass了內(nèi)置的Cocoa類 UIViewController(你很可能這么做),然后你給了這個(gè)子類一個(gè)構(gòu)造器(你很可能這么做):
這個(gè)代碼就不會編譯。編譯錯(cuò)誤顯示:required initializer init(coder: ) must be provided by subclass of UIViewController.
我們現(xiàn)在就知道其中的緣故了:UIViewController采用了一個(gè)NSCoding 的協(xié)議,而這個(gè)協(xié)議需要一個(gè)構(gòu)造器 init(coder: )。這都不是你造成的,UIViewController 和NSCoding都是Cocoa定義的。但是這不礙事,這就是我剛剛提到的情形。你的UIViewController子類要不就是繼承了init(coder:) 要不必須顯式實(shí)現(xiàn)它并且標(biāo)記required。既然你的子類已經(jīng)有了一個(gè)指定構(gòu)造器,那么繼承就泡湯了,只能用后者了。
但是如果你壓根兒沒想過要實(shí)現(xiàn)這么個(gè)構(gòu)造器,再這樣操作這就顯得沒有意義了。Xcode的Fix-It特性提供了下面的方法:
它既滿足了構(gòu)造器(在c5,會有為什么即使他沒有完成條件,但依然是合法構(gòu)造器的原因),又使得他在被調(diào)用的時(shí)候會自動crash。
如果你真的有要實(shí)現(xiàn)的東西,那么只需要?jiǎng)h除fatalError 一行,并且用你自己的代碼代替就可以了。最小的實(shí)現(xiàn)內(nèi)容就是super.init(coder: aDecoder),當(dāng)然如果你有需要初始化的屬性,你也可以先初始化它們。
不僅UIViewController,還有很多Cocoa的內(nèi)置類都采用了NSCoding。如果你有它們的子類并且要自己初始化構(gòu)造器,那么會經(jīng)常遇到這個(gè)問題。
字面轉(zhuǎn)換:
Swift中的一個(gè)非常好的點(diǎn)就是:它的很多特性是在內(nèi)部實(shí)現(xiàn)的而且在頭文件中可以看到。Literal就是一例。比如你可以直接寫出5來制造一個(gè)值為5的Int(而不需要很正式地寫Int(5))不是因?yàn)樾W(xué),而是因?yàn)镮nt采用了IntegerLiteralConvertible協(xié)議。不只是Int Literal 是這樣工作,其他Literal也是這樣的原理。下面是在Swift頭文件中定義的literal convertible protocol:
你自己的對象類型也可采用上面的轉(zhuǎn)換協(xié)議。這意味著literal可以出現(xiàn)在你的對象類型實(shí)例可以出現(xiàn)的地方。
比如我們定義一個(gè)Nest類型,其中包括雞蛋個(gè)數(shù)變量:
因?yàn)镹est采用了IntegerLiteralConvertible,我們可以將Int傳至Nest可以用的地方,然后init(integerLiteral:)就會被自動調(diào)用,從而產(chǎn)生一個(gè)新的Nest對象包含特定的雞蛋數(shù)。