Swift 泛型歷史
我們首先來回顧一下 Swift 中對於泛型支持的歷史變更,看看現在在 Swift 中,泛型都支持哪些特性
Swift 泛型是 Swift 語言中的一個重要特性,在歷屆 WWDC 大會都有被提及,網上可以參考的資料也很多。這次會議上討論了泛型特性的一些設計思路
泛型對於 Swift 的重要性
考慮一個如下的一個集合類型
對於這樣的一個集合類型,我們並不能定義他的 get/set 方法對應的變量類型,充其量只能定義一個萬能類型(如 OC 中的 id 或 C++ 中的 void * )。Swift 中也有這樣的一個萬能類型 Any ,但是這樣會帶來非常不好的開發體驗,看看下面的這個例子
對於 words 變量而言,可能你一直把它當作一個字符串數組來使用,但是實際上有可能在別處塞入了一個非字符串類型的變量進去,從而導致強制解包失敗引起程序崩潰,這是一個令人心塞的經歷。
實際上,對於以上的例子,內存管理上的問題就更為突出。
譬如對於一個整型數組來說,他的內存佈局是非常緊湊的
如果是一個 Any 類型的數組,內存佔用就會變得很大,因為這個時候需要預留足夠多的內存空間給所有可能的變量類型使用,這樣就是一個極大的浪費。
考慮一下如果用 Any 來包裹一個值類型的變量的話,內存佈局上將會更加複雜了
在 OO 語言的時代,要解決以上問題的話,一般採用參數多態(Parametric Polymorphism)技術,在 Swift 中而言,就是泛型
譬如對於上述的例子,我們用Swift泛型來定義的話,應該是這個樣子
這樣子,我們就可以告訴編譯器,Buffer 中應該包含有哪些類型的變量。因此,如果寫了錯誤的代碼的話,編譯器就會馬上報錯,如
對於泛型類型來說,在初始化的時候,編譯器如果擁有足夠的信息去推導變量類型的話,我們可以不用顯式聲明泛型的具體變量類型,如
這樣子的話,我們就能對於不同的變量類型,也能做到內存中的緊湊佈局,而不會浪費掉不必要的內存空間
基於類型推導技術,我們就能對泛型寫更加便利的代碼了,譬如
然而,對於 Buffer 來說,如果直接按照 Int 的預設去寫代碼的話,依然會遇到編譯錯誤,譬如
要解決這個問題,我們只需要給泛型類型添加約束即可
或者我們稍微擴展一下,定義協議也是可以的,這個時候就不僅限於 Int 的使用了
協議設計
在上面的例子,我們已經創建了一個泛型類型,但是我們的抽象化還不夠。如果我們想要把泛型適配的範圍擴展得更大一點,我們需要用協議去定義行為。我們以大家熟悉集合類型來看看怎樣定義一個合理的集合協議
如果我們要為集合類型添加下標操作的話,可以這樣子做
實際上,這樣的設計過於簡單化了,考慮一下 Array 和 Dictionary 的場景,對於 Array 而言,通過下標去尋找元素是比較容易而且實現也是顯而易見的,但是對於 Dictionary 而言,我們還需要合適的包裝,譬如
基於以上的考慮,我們可以將集合的協議再通用化聲明一次,看看效果會不會更好
這裡我們考慮到了下標操作的各種場景,並且用更靈活的 Index 類型去定義下標操作對應的索引行為,這樣子基本覆蓋到了我們日常遇到的場景,而將更多變的實現方式留給了具體的算法實現代碼中。舉個例子
或者我們可以高效一點,將約束條件直接寫到協議中去,這樣子我們就不需要針對每一個具體的泛型類型去定義相對應的約束條件了
定製點( Customization Points )
我們來考慮對於同一個協議中的函數聲明,我們可以有不同的實現方式,如
當我們要使用 count 的時候,大部分的場景下,我們只想調用簡單實現,並不太在意性能,而有些時候我們又希望能夠採用高性能的解決方案,在泛型中,我們可以引入定製點的概念,從而滿足各種定製化場景的需求
在以上的例子,我們在協議聲明的時候,預埋了 count 函數的默認實現方式,而對於 Dictionary 類型來說,將會採用更好的 count 實現方式,從而在保持泛型聲明的前提下,優雅地封裝了同一函數的不同實現方式。
協議繼承( Protocol Inheritance )
Swift 提倡一種面向協議的編程方式,因此,我們很自然地把以前在面向對象編程中的一些很好的特性遷移到協議的設計中,考慮一下當我們需要對上述的集合協議擴展一些特殊的功能時,譬如 lastIndex 、shuffle 等,這個時候使用繼承是比較合適的。用一個具體的代碼表示如下
在面向協議編程的設計中,我們要時刻記得,協議是用於定義行為確定,但實現各異的場景,而不是過於特化的去設計接口,我們來看一個不好的設計方式
仔細考慮一下,對於 ShuffleCollection 而言,需要的是兩種行為
隨機訪問元素
修改內部變量實現 shuffle
因此,通過定義粒度更細的協議,可以讓上訴行為展示出更多的多態性
合理的協議設計思路,我們可以用如下的類圖來表達
自上而下看,泛化的協議會變得越來越特化,對應于越來越窄的適用場景(然而這個時候還是要儘可能確保場景適用面廣),從下往上看,超類的協議總是能夠把更多態的行為抽象出來表達。協議的繼承關係應該比類的繼承關係更用心地去設計,儘可能地保留協議行為的原子性和多態性,讓代碼更加易於擴展和組裝。
條件一致性( Conditional Conformance )
條件一致性在 Swift 4.1 中引入,表達了這樣的一個語義:泛型類型在特定條件下會遵循一個特定的協議,譬如
在 Swift4.2 中,條件一致性能力的增強,使得我們可以做更多的事情。譬如多協議的條件一致性檢查也是可以的
泛型和類怎樣抉擇
Swift 是一個多編程範式的語言,你既可以將它用於面向協議編程(POP),也可以用於面向對象編程(OOP)。
如果採用對象繼承的方式,要時刻記得『里氏替換原則』,我們來重溫一下它的具體概念
里氏替換原則( Liskov Substitution Principle LSP )面向對象設計的基本原則之一。 里氏替換原則中說,任何基類可以出現的地方,子類一定可以出現。 LSP 是繼承複用的基石,只有當衍生類可以替換掉基類,軟件單位的功能不受到影響時,基類才能真正被複用,而衍生類也能夠在基類的基礎上增加新的行為。
因此,繼承的原則是:繼承必須確保超類所擁有的性質在子類中仍然成立。也就是說,當一個子類的實例應該能夠替換任何其超類的實例時,它們之間才具有is-A關係,即構成繼承關係。
簡單的代碼演示如下
不過在實際編程中應用 OOP 常常會遇到這樣的困境:超類的設計往往跟子類有關,如果當一個新的子類出現,是修改超類行為從而覆蓋新的子類呢,還是變成組合關係?
我們使用 POP 的話,結合泛型可以提供了行為的多態性,在某種意義上來說對於是一種更柔性的解決方案。在 Swift 中,協議可以擁有默認實現,可以增加約束,可以有條件一致性提供查詢,最重要的,還能提供繼承關係。這些特性都可以幫助我們更好地在 POP 下重用代碼。
在 POP 下,里氏替換原則依然適用於協議的繼承關係,如下述代碼所示
下面我們考慮一下 POP 下的工廠模式實現,這是一個教科書式的例子來幫助我們理解 Swift 中,協議相關的強大特性
最後,在 Swift 中我們還可以用 final 來修飾一個類,用於表達這個類是不能被繼承的
總結
本次 Session 探討了很多泛型和協議相關的話題,我們來簡單回顧和總結一下要點
1.泛型設計對於 Swift 語言來說是一個很重要的特性,能夠既保持靜態類型的特點又能夠達到代碼重用的目的
2.協議的設計要遵循自上而下和自下而上的原則
3.自上而下:協議的繼承表達了更特化的行為描述
4.自下而上:父類協議應該能夠將更抽象的行為進行封裝從而達到代碼重用的效果
5.繼承的設計要遵循里氏替換原則
閱讀更多 今日頭條技術團隊 的文章