一、OC 協(xié)議:發(fā)消息
OC 的協(xié)議本質(zhì)是消息的集合。例如,UITableViewDataSource 協(xié)議有請求 sections 個數(shù)和一個 sections 中的 rows 的個數(shù)。
二、Swift 協(xié)議:消息 + 關(guān)聯(lián)類型
Swift 協(xié)議也是消息的集合。Swift 的協(xié)議同時也包含關(guān)聯(lián)類型。協(xié)議中的關(guān)聯(lián)類型是類型占位符,實現(xiàn)協(xié)議的時候來填充具體的類型。
三、關(guān)聯(lián)類型:簡化協(xié)議的實現(xiàn)
關(guān)聯(lián)類型是一個強大的工具,讓我們更方便的實現(xiàn)協(xié)議。
例子:Swift Equatable 協(xié)議
例如,Swift Equatable 協(xié)議有一個比較兩個值是否相等的函數(shù)。
static func ==(lhs: Self, rhs: Self) -> Bool
這個函數(shù)使用了 Self 這個關(guān)聯(lián)類型。Self 的具體類型是由協(xié)議的具體實現(xiàn)來確定。假設(shè)有一個類型下面的結(jié)構(gòu)體,那么這個時候 Self 就是 Name。
struct Name: Equatable {
let value: String
}
這個時候,協(xié)議的具體實現(xiàn)如下:
static func ==(lhs: Name, rhs: Name) -> Bool
綜上:Equatable 使用關(guān)聯(lián)類型 Self 來約束比較相同類型的值的 == 函數(shù)。
四、對比:OC NSObjectProtocol
NSObjectProtocol 也有一個 isEqual(_:)方法,但是因為是 OC 的協(xié)議,不能用 Self 類型。具體的定義如下:
func isEqual(_ object: Any?) -> Bool
OC 的協(xié)議無法約束參數(shù)類型為指定關(guān)聯(lián)類型,因此所有遵守協(xié)議的類型都能用來比較。通常在實現(xiàn)中會先檢測參數(shù)的類型與消息的接收者類型是否一致。
func isEqual(_ object: Any?) -> Bool {
guard let other = object as? Name else { return false }
// 開始檢查值是否相等
}
每一個 isEqual(_:) 的實現(xiàn),在每次調(diào)用的時候都要做這樣的檢查。Equatable 協(xié)議不會做這樣的檢測,通過 Self 關(guān)聯(lián)類型保證了對象滿足條件。
五、代價
Protocol ‘SomeProtocol’ can only be used as a generic constraint because it has Self or associated type requirements.
因為需要實現(xiàn) Self 或者其他關(guān)聯(lián)類型,該協(xié)議只能用做泛型約束。
關(guān)聯(lián)類型是個強大的工具。所付出的代價就是
error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
協(xié)議中使用關(guān)聯(lián)類型,必須用做泛型。
泛型也是一個占位符,調(diào)用泛型函數(shù)的時候,必須填充對應(yīng)的類型。
從泛型和關(guān)聯(lián)類型的調(diào)用和實現(xiàn)兩個方面對比來看:
關(guān)聯(lián)類型:實現(xiàn)方指定類型,調(diào)用方不指定。當(dāng)你實現(xiàn)一個使用關(guān)聯(lián)類型的函數(shù)的時候,你需要填充對應(yīng)的類型,所以你知道實際的類型。調(diào)用方不知道你具體采用的類型。
泛型:調(diào)用方指定類型,實現(xiàn)方不指定。當(dāng)你實現(xiàn)一個使用泛型的函數(shù),不需要知道調(diào)用方具體采用的類型。你可以使用約束限制類型,但是你必須處理所有滿足約束的類型。調(diào)用方指定具體的類型,但是你的代碼必須處理傳遞的任意類型。
例子: 調(diào)用 Equatable == 強制使用泛型
func checkEquals(left: Equatable, right: Equatable) -> Bool {
return left == right
}
Swift 編譯器調(diào)出如下錯誤:
error: MyPlaygroundSwift.playground:27:24: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
func checkEquals(left: Equatable, right: Equatable) -> Bool {
^
error: MyPlaygroundSwift.playground:27:42: error: protocol 'Equatable' can only be used as a generic constraint because it has Self or associated type requirements
func checkEquals(left: Equatable, right: Equatable) -> Bool {
為什么不使用泛型,checkEquals 不起作用?
如果 swift 允許這樣做會怎么樣,我們來做個實驗。
假如有兩個遵守 Equatable 的類型,Name 和 Age。代碼可能會是這樣。
let name = Name(value: "")
let age = Age(value: 0)
let isEquals = checkEquals(name, age)
這樣做沒有任何意義,從以下兩個方面來看:
行為:怎樣運行這段代碼?怎樣實現(xiàn) checkEquals 中的 == 調(diào)用。只有 (Names, Names) 和 (Age, Age) 才有意義,因為 Equatable 定義為 ==(Self, Self)。只調(diào)用 Name 或者 Age 的 == 會打破類型安全。
意義:有什么意義? Equatable 協(xié)議不僅僅是個類型,它還和另一個類型 Self 有關(guān)。如果你寫
checkEquals(left: Equatable, right: Equatable),只是在討論Equatable,它的關(guān)聯(lián)類型Self被忽略。不能只是明確Equtable,必須明確Equtable where Self is (some type)
這非常蛋疼,但是很重要,checkEquals 看起來會工作。你想比較兩個 Equtable 類型。但是Equtable是一個不完整的類型,它其實是equtable for some type
checkEquals(left: Equatable, right: Equatable)中左邊是一個equtable for some type,右邊也是一個equtable for some type。左右兩邊并不是equatable for the same type。Equatable ==需要左右同一個類型,所以上面的情況checkEquals不起作用。
使 checkEquals 處理所有的 Equatable + Self
checkEquals在Equatable where Self is (some type)不知道具體的some type。所以,必須處理所有的Equatable 和 Self 類型,checkEquals所有的類型T,T是Equatable和它的關(guān)聯(lián)類型
具體的代碼如下:
func checkEquals<T: Equatable>(left: T, right: T) -> Bool {
return left == right
}
現(xiàn)在,類型 T是一個Equatable類型,T的關(guān)聯(lián)類型Self,實現(xiàn)了checkEquatable方法。使用 Swift 的泛型為具體的類型實現(xiàn)了一個配方,不需要寫具體的checkEquals(left: Name, right: Name)和checkEquals(left: Age, right: Age)。最后需要提取泛型函數(shù)來重構(gòu)代碼。
例子:調(diào)用 NSObjectProtocol 的 isEqual(_:) 不需要泛型
使用NSObjectProtocol的checkEquals不需要使用泛型。
import Foundation
func checkEquals(
left: NSObjectProtocol,
right: NSObjectProtocol
) -> Bool {
return left.isEqual(right)
}
寫起來很簡單,這種情況下還是允許我們以下調(diào)用:
let isEqual = checkEquals(name, age)
Name 可以和 Age 比較??當(dāng)然不能,isEqual結(jié)果是false。Name.isEqual(_:) 會判斷對象是不是 Name 類型。不像Equabable的==,所以每個isEqual(:)方法必須處理類型一致性。
六、權(quán)衡
關(guān)聯(lián)類型使 Swift 的協(xié)議比 OC 的更加強大。
OC 的協(xié)議捕獲了對象和它的調(diào)用者間的關(guān)系。調(diào)用者能夠使用協(xié)議發(fā)消息,消息的具體行為由遵守協(xié)議的對象來實現(xiàn)。
Swift 協(xié)議也能夠捕獲一個類型和多個關(guān)聯(lián)類型之間的關(guān)系。Equatable協(xié)議通過Self關(guān)聯(lián)一個類型。SetAlgebra協(xié)議將實現(xiàn)關(guān)聯(lián)到類型Element。
通過對比Equatable的==和NSObjectProtocol的isEqual(:),能夠發(fā)現(xiàn) Swift 實現(xiàn)協(xié)議比較簡單,但是在使用協(xié)議的時候比較復(fù)雜。強大的實現(xiàn)也會使代碼變得很復(fù)雜,所以在使用協(xié)議的時候,需要權(quán)衡使用關(guān)聯(lián)類型的價值和處理他們的復(fù)雜程度。
希望通過這篇文章幫助你認(rèn)識使用的協(xié)議和 API。如果你覺得有用,可以關(guān)注我們的高級 Swift 訓(xùn)練營。
七、刨根問底:Self 是關(guān)聯(lián)類型么?
Self 的行為很像關(guān)聯(lián)類型,不同于其他關(guān)聯(lián)類型,不需要指定 Self 關(guān)聯(lián)的類型,Self 會自動關(guān)聯(lián)到實現(xiàn)協(xié)議的類型。
但是協(xié)議中的錯誤提示是“有 Self 或者關(guān)聯(lián)類型”,聽起來很像是不同的類型。
為了找到答案,我找到了 Swift 編譯器中 AST 的源碼,具體的文檔在這里
每個協(xié)議有一個隱式構(gòu)造的關(guān)聯(lián)類型 Self,描述了遵守協(xié)議的類型。
所以,Self 是一個關(guān)聯(lián)類型
八、感謝原作者Jeremy Sherman,原文地址
https://www.bignerdranch.com/blog/why-associated-type-requirements-become-generic-constraints/