我從55個Swift標準庫協議中學到了什麼?

發表於2016-01-07
Swift團隊使用協議的方法,給了我們哪些使用協議的提示?

好的。55個Swift標準庫公有協議,18分鐘,讓我們開始吧。

首先我只想問:為什麼是協議?為什麼面向協議程式設計?如果我們回到過去那段年少無知少不更事的面相物件程式設計時期,我們很多人最初學習的是Objective-C,這意味著我們免受多繼承的專橫。又或者你是這個房間裡另一半喜歡C++的人,那麼我們並沒有受過多繼承的啟示,我們稍後將對其進行討論。

單繼承中,層次結構是線性的:你有父輩、子輩以及孫子輩一系列的繼承樹。當你回到樹的頂端,所有的一切有一個單獨的父輩。這使得層次乾淨,但同時你的確失去了合理使用多繼承所帶來的優勢。在Swift中不能繼承列舉和結構型別,只有類可以。這意味著你有時需要弄得跟麻花一樣來讓你的型別有意義。這樣最終能得到真正通用的超類。然後一級一級下來,如果你可以想象更多的級在你從圖中獲得一個葉子結點之前,你才能得到一個真正可以例項化和使用的類。

“你有時需要弄得跟麻花一樣來讓你的型別有意義”

所以通過協議,你可以使得型別系統更加有組合性,你可以理清繼承的長鏈。當然你將要放棄一個長而高的繼承鏈,為了一個有協議一致性的寬的鏈。但我認為取捨是值得的,希望在這個演講結束後你也這麼認為。

什麼樣的東西放進協議裡有意義?我不打算談我已經完成的很酷的協議,而是瀏覽一下蘋果在Swift標準庫中提供的協議。我們將瀏覽,也許你會學到一些你以前從未聽過的協議。我們究竟能找到什麼偉大的想法,接著也許可以獲得一些靈感——在我們的程式碼裡什麼樣的東西可以和協議一起用。基本上這個演講的思路是Swift團隊使用協議的方法,給我們一些可以怎麼用協議的提示?

最終目標是讓你開始思考協議,想出一些很酷的點子然後開源,然後我會在你GitHub的倉庫上點星。

三種協議類別:”Can do”,”Is a”以及”Can be”

Swift標準庫包括54個真正的共有協議。一開始這個演講我開玩笑地想一個個說明它們,每個16秒來湊時間,但相反,我把它們分成三類。”Can do”協議,”Is a”協議,”Can be”協議。每次我們會看其中一個,看一些示例,再看看我們能夠學到的東西。

“Can do”協議

首先我們先來看看”Can do”協議。這些描述的事情是型別可以做或已經做過的,它們也以 -able 結尾為語法,這使得當你瀏覽標頭檔案時它們很容易被找到。第一個例子:一個型別遵守Hashable協議,意味著你可以得到這個類的整型雜湊值。這意味著你可以把它儲存進一個集合,可以當作一個字典的鍵等等。還有Equatable和Comparable協議,這意味著你可以用Swift中各種相等和比較的運算子比較兩個例項的值。這些都是很常見的協議,你可能已經在你自己的型別中實現了。你會注意到這些協議描述了你可以對類進行的操作。有比較、相等、雜湊。

“AbsoluteValuable(絕對有價值)。這聽起來如此重要。正因為它以’valuable(價值)’結尾”

這裡做一個補充說明,讓我們來談談我認為的最好的命名協議,它值得一個特殊的過度動畫:AbsoluteValuable(絕對有價值)。這聽起來如此重要。正因為它以”valuable(價值)”結尾。但不幸的是它沒有聽起來那麼重要。它只意味著支援絕對值函式。

還有一個小協議子集在這個”Can do”組中,和替代檢視或替代表示形式有關。

讓我們看看一個簡單的例子RawRepresentable。這意味著該類可以表示成某種原始值,然後你可以把原始值轉變回實際的例項。這聽起來很像Swift裡的列舉,有內建的原始值。所以所有有列舉的功能的類有一個初始化函式,它接受一個原始值,一旦你有一個列舉值就得到它原始值的版本。這些都建立在這個協議之上。你可以用自己的結構體和類做類似的事情,如果你喜歡。這裡的想法是,東西內在的值是一樣的,你只是改變它外在的表示。只因為,原始值和例項值之間存在一對一的對映。

同屬此類。

接下來是CustomPlaygroundQuickLookable。這只是意味著你型別可以在playground中快速檢視。同時這也意味著,你的型別是一樣的,你不是將它轉換為其他東西,而是為你的值提供另一種替代檢視。在這種情況下,它可以在快速瀏覽中顯示。

“所以我們有操作,我們有替代試圖。從中我們可以學到什麼呢?”

如果你自己的型別有操作,比如說你曾正在寫下一代Instagram殺手級照片過濾APP,你可以新增一個過濾性的協議,然後讓你的照片例項、影象去遵守它。假設之後,你的過濾APP火了,它真的流行起來,你想要擴充套件到視訊。然而視訊只是另一種形式的媒體。在理論上,你還可以應用過濾性的協議在視訊、音訊、3D照片上,無論將來出現了什麼。

替代檢視的例子呢?總存在從大照片建立縮圖的情況,你可以認為這是全尺寸照片的替代檢視。再次,這實際上不是一個轉換,它只是替代的表示形式。所以,你可以想象這樣一個Thumbnailable協議,希望你能想出一個更好的名字,甚至音訊版本的一個縮圖。縮圖就像一個低位元率版本的音訊之類的。

這裡的基本思想,是把你APP和程式碼中常見的操作抽象出來,協議化他們,如果有這個詞。為什麼你要這麼做?一個好處是使得想法可重用。你有幾種型別需要實現一些常見的操作,現在他們可以共享同一個有保證的公共的介面。你可以得到多型的好處,即使是在你的結構和列舉裡。而且,這種複合的方法可以幫助你從操作中分割東西出來。我知道這裡意見會不同,但我喜歡從這樣的小塊來建立一個型別,基於它們能做什麼。這是第一類的協議、操作、替代檢視。你可以建立的操作和檢視的集合用於您的型別。

“Is a”協議

下一類是”Is a”協議,這些描述型別是什麼樣的。與”Can do”的協議相比,這些更基於身份。這意味著遵守多個協議,感覺很像Swift的多繼承。您可以在標準庫中找到這些協議,因為它們以”-type”結尾。它們佔了整整一半標準庫,54箇中有35個左右的是這類的。讓我們來看一個例子。CollectionType是個好協議。顯然Array、Dictionary、Set遵守CollectionType。或許更令人驚訝的是,Ranges和String views。如果你有一個字串,你可以獲得它的UTF-8或UTF-16的表示形式。所以,它只是一系列的Unicode程式碼點。所以,它也遵守CollectionType。

還有一些協議包括的一些原語像IntegerTyp、FloatingPointType、BooleanType等等。這些協議更像分組。所以有幾個整數型別。我們有無符號整數、有符號整數、16位整數等等。但它們都組合在一起,因為他們共同遵守整數型別的協議。如果你想出自己的整數型別,也許你想要一個4位整數或6位整數,有點非主流,那麼為什麼不遵守這個協議。但是,這不大可能發生。大多數這類協議你可能從來沒有編寫型別去遵守這些協議。例如你看BooleanType標頭檔案的註釋,實際上勸阻你創造更多的布林型別,因為一個已足夠。另一個例子是MirrorPathType,它的標頭檔案有以下令人愉快的註釋:”不要宣告新的類遵守這個協議,它們不會像預期的那樣工作。”

所以,正如你所看到的,這意味著許多這一類的協議,你可能是使用遵守這些的型別。我相信每個人都使用一個陣列或一個整數,但你可能不會建立遵守它們的型別。雖然有一些你可能使用——我們有ErrorType,我們早些時候聽Thomas說過,在Swift 2中新的錯誤處理模式用。還有SequenceType、GeneratorType,如果你正在構建集合化的東西且你想要支援迭代,可以看一看這些協議。

這就是”Is a”協議。協議被當作身份。我們可以從這個模式的協議中學到什麼嗎?再次,因為這些都是基於身份而不是操作,你可以在更大的型別分組中使用它們。回到規範化動物王國的例子:這是一個誇張的很長的類層次結構。底部甚至沒有一種我們可以例項化的型別。在這個類層次結構上,每一步都比之前新增一些功能。因此,有了協議你可以讓你的型別系統有更多的組合性。你有這個協議的清單,你可以構建和在不同型別中使用。比如貓叫和狗吠,更多是一種”Can do”風格的動物能做的事,而”兩條腿”和”四條腿”則更多是一種身份型別。你會注意到兩條腿和四條腿也有繼承,因為協議可以像這樣繼承。

這意味著,一旦你設定好了這些協議,你可以建立起過去需要巨大的超類列表而現在只是一組協議的東西,包括繼承如果你需要它。當你構建你的型別,你可以在這裡選擇身份和需要的功能。因為你的型別可以遵守多個協議,你可以一點點建立型別的功能,基於協議的一致性。

這就是第二類的標準庫協議–“Is a”協議,與分組和身份有關。

“Can be”協議(11.28)

最後我們有”Can be”型別。這不是同一個東西的替代檢視,正如我們已經看到的,這些都是直接轉換。從型別X轉換到Y。這些協議以”-Convertible”結尾。這意味著這個型別可以被轉換到或者轉換成別的東西。

讓我們看看幾個例子。我們有簡單的初始化風格的,如FloatLiteralConvertible、IntegerLiteralConvertible、ArrayLiteralConvertible等等。如果你的型別遵守FloatLiteralConvertible那麼這意味著你需要一個初始化函式,接受某種floatLiteral,預設情況下這是一個double值,然後構建你的型別。所以轉換的方向是從一個浮點數到你的型別。

相比之下,有每個人的最好的朋友CustomStringConvertible之類的協議,又或如先前知道的Printable協議。它指定你的型別可以轉換成一個字串,所以轉換變到另一個方向,是從你的型別中到一個字串。

“一個Objective-C從業者看到,說’啊,那沒什麼。我碼方法宣告的時間比每次我碼程式碼的時間都長。'”

這裡再次補充說明,54個協議中名稱最長的也在此類,ExtendedGraphemeClusterLiteralConvertible——41個字元。我相信那些來自Objective-C會說、或笑、就像:”啊,那沒什麼。我碼方法宣告的時間比每次我碼程式碼的時間都長。”這就是用於轉換的協議在這最後一組。我們可以從這樣的協議中學到什麼,除了要儘量保持你型別的名字短?

這是很明顯的。如果你有型別,它可以成為其他型別,那就不要只新增一個函式,或新增一個被計算的屬性,或新增一個初始化函式。考慮設定一個協議。記住你可以使用協議指定你的型別被轉換到或轉換成別的。所需的其他任何技術討論的例子,除了動物,是員工資料庫。如果你有物件表示人、普通員工、經理、承包商,那麼這些人可能是一個單獨的型別。如果承包商可以被僱傭而作為一個員工,或員工可以被晉升為經理,那這就是一種轉換。你不想再加入人的名稱、地址、電話號碼和社會安全號等等。你想相對無縫地把一個承包商轉換成員工。你可以用一個EmployeeConvertible協議。然後說承包商型別和應試者型別能遵守它。

這麼做的好處是什麼?為什麼需要一個協議加上轉換函式,而似乎僅僅一個函式不是更簡單嗎?再次,它一部分是組合的方法。一個應試者可以成為員工的事實,是型別是什麼的一部分,但這並不特別。其他的人也可以成為員工。通過使用一個協議可以保證有一個共同的定義良好的介面來將一些人轉換成一個員工。

還有一個好處是,程式碼漂亮的像文件一樣。如果你瀏覽程式碼,或者專案裡的其他人,你會看到”EmployeeConvertible”,並且你已經熟悉它,它告訴了你這型別能做什麼和介面是什麼樣子。你還可以在你的專案查詢”EmployeeConvertible”這個詞,然後在搜尋結果中,就可以看到能成為員工的型別的列表。這就是”Can be”協議組合,處理你型別之間的轉換。

四個廣泛模式

所以我們看到三類來自標準庫的協議,它們與能力、身份和轉換有關。什麼是廣泛的模式,想想我們自己的程式碼?我們有四個:

  • 操作–如果有一組通用的操作你必須在型別中執行,考慮抽象出來當作協議。
  • 與替代檢視有關–如果你的型別有替代檢視,或另一種表示形式,不是一個完整的轉換,想想它是否遵守公有協議。
  • 代表身份–這是你做類似多繼承的地方,或混合多種型別(Mix-ins)的型別。考慮身份和型別,把相似的型別用協議來分組。
  • 最後我們有轉換,無論是從一個型別轉換成別的,還是一個型別被轉換到,如果特定的轉換在程式碼中多次發生,考慮抽象很常見的轉換作為協議,這幫助你跟蹤事物,並保持一致的介面。

我想,看到蘋果把如此多的常用功能,如對映、過濾器、列舉函式、抽象到協議,使用的也只是普通的舊協議和協議擴充套件,這是一個很好的例子。一個未來將如何強大的靈感。蘋果正遵循這個榜樣。例如,如果你看看陣列的定義,它遵守8個協議,字串遵守12協議,等等。所以這裡的想法是,你建立這些特徵打包在協議裡,然後你的你的程式碼庫就都可以使用。

我認為以這種方式思考你的型別,可以幫助你在腦海裡保持清晰和並把這些型別分類。所以我肯定鼓勵在你自己的程式碼裡嘗試這些。仔細看看你的型別,聚焦於它們的共同點,看能不能用協議。

以上就是我全部要說的。協議萬歲,謝謝!

Q&A (17:47)

Q1:你好,又是我–Dave Ungar。演講讓我思考我自己的程式碼,當不用協議時。例如我有一個結構體,它有10個函式。也許我應該用10個協議。具體來說來自標準庫,並沒有字串的協議。因此我不得不創造一個,為了把一個Where子句轉換成別的一般化的東西,引數可以是一個字串。所以你能說說看,是何時不該使用協議,還是為什麼當我需要時沒有一個字串協議?謝謝。

Greg:蘋果就是這樣,對吧?從不滿足每個人的需求。但是我認為何時用何時不用,我正在使用的方法是根據普遍性來分類。如果我只有一個型別遵守這個協議,很大程度上我就留給這個型別去處理。如果這個型別做10件事那就做10件吧。但如果它甚至發生兩次,只要不止一次,對我來說這就足夠去說”好吧,這是普遍得足以發生兩次”,似乎是一個奇蹟的重複,也許這將發生不只一次。所以這是一個主觀判斷的問題。但是我想說,不止發生一次是至少的要求。

觀眾:天哪。所以值得複製函式、宣告和所有的東西去得到協議。

Greg:沒錯,就是這樣。

Q2:你好。我想知道,你處理過泛型協議嗎?如果有,你的策略是什麼?

Greg:我認為我有這樣一個例子,因為協議不能有泛型,但你可以取型別別名,正如你常看到的,你只是定義型別別名。如果你想,你可以稱之為型別別名T。如果你想讓它看起來像泛型。然後當你定義型別你只需要定義型別別名作為具體型別。這是我在協議裡見過的方法。

觀眾:我認為,有時情況就變得有點困難,比如如果你宣告一個變數,然後你說”我想讓這個變數遵守這個協議。”如果這協議有一個型別別名,你不能這麼做,因為編譯器不喜歡它。

Greg:是這樣,它更像是如果你定義你自己的型別,然後你可以給型別一個別名那麼這也是一種選擇。但我還沒找到一個更好的在變數情況下的通用解決方案。

Q3:非常感謝你這次演講,我很喜歡它。這樣思考協議似乎讓我更加清晰了。我的問題是,你說的有4次你可以使用協議的地方,或思考協議的地方,操作,替代檢視,身份和轉換。我的問題是,你能說清一下替代檢視,替代試圖和轉換之前的區別。我通常思考的方式是,有時我可以將型別轉換為替代檢視,但是你似乎把它區分開來了,對吧?

Greg:好的,我聽懂了你的問題。問題的一部分是因為協議的命名方式。也許它們的命名糟糕。再次,Printable曾是一個 -able 協議,現在卻是CustomStringConvertible。這是臨陣倒戈,對吧?但我認為,我看它是以替代檢視的含義,事物本身並沒有改變,只是外在的樣子正在發生變化。從全尺寸影象到縮圖,或從列舉值到原始值,它們其實是一樣的。只是你透過一個不同的鏡頭。然而轉換協議更像是”我有一個整數,我將變為一個字串。”這是CustomStringConvertible,對吧?你把它改變成了一個完全不同的型別,這不僅僅是一個有自己東西的不同的檢視,它會有自己的生命週期。我就是這樣看的。

相關文章