為什么 Swift 關(guān)聯(lián)類型的協(xié)議需要做為泛型約束使用(譯)

一、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 typeEquatable ==需要左右同一個類型,所以上面的情況checkEquals不起作用。

使 checkEquals 處理所有的 Equatable + Self

checkEqualsEquatable where Self is (some type)不知道具體的some type。所以,必須處理所有的Equatable 和 Self 類型,checkEquals所有的類型T,TEquatable和它的關(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(_:) 不需要泛型

使用NSObjectProtocolcheckEquals不需要使用泛型。

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==NSObjectProtocolisEqual(:),能夠發(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/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容