為什么 Swift 關聯類型的協議需要做為泛型約束使用(譯)

一、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

checkEqualsEquatable where Self is (some type)不知道具體的some type。所以,必須處理所有的Equatable 和 Self 類型,checkEquals所有的類型T,TEquatable和它的關聯類型

具體的代碼如下:

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(_:) 不需要泛型

使用NSObjectProtocolcheckEquals不需要使用泛型。

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374