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