語法的集合股耽?協(xié)議可沒那么簡單"
作者:Ole Begemann,原文鏈接褐健,原文日期:2016-12-30
譯者:Cwift付鹿;校對:walkingway;定稿:CMB
本周在 Swift 進化 板塊蚜迅,有一個有趣的(且爭論很久的)討論舵匾。 有人建議在 Swift 標準庫中添加一個名為 DefaultConstructible 的協(xié)議,其唯一的要求是提供一個無參數(shù)的構(gòu)造器:
protocol DefaultConstructible {
init()
}
換個說法谁不,用協(xié)議的方式規(guī)范以下概念:你可以創(chuàng)建某種類型的“默認”值坐梯,或者說當沒有附加信息時也能得到一個遵守協(xié)議的實例。
有一些人刹帕,比如 Xiaodi Wu 和 Dave Abrahams吵血,提出了一些非常好的論據(jù)來反對這個觀點。在這里我再次重復一次這些觀點偷溺,因為我覺得相比這個具體的話題蹋辅,他們所討論的內(nèi)容有著更加廣泛的意義。
語義是協(xié)議的重要部分
第一點是協(xié)議不僅僅只是語法的集合挫掏。協(xié)議的語義與其提供的接口一樣重要侦另。
Xiaodi Wu:
在 Swift 中,協(xié)議不僅僅確保特定的拼寫砍濒,而且還確保特定的語義淋肾。
我應該補充一點硫麻,這是 Stepanov 提出的編程通用的核心原則爸邢。
以及:
協(xié)議(也稱概念)不僅僅是語法的集合;如果不能給操作增加語義拿愧,那你就不能為它們編寫有價值的通用算法杠河。因此不應該使用 DefaultConstructible,類比一下,你不應該使用 “Plusable” 來表示遵守者可以進行 x + x 格式的操作券敌。
Equatable 協(xié)議的語義
以 Equatable 協(xié)議為例唾戚。它的 API 是很輕量的 —— 只有一個單一的函數(shù):
public protocol Equatable {
/// Returns a Boolean value indicating whether two values are equal.
///
/// Equality is the inverse of inequality. For any values `a` and `b`,
/// `a == b` implies that `a != b` is `false`.
static func == (lhs: Self, rhs: Self) -> Bool
}
不過,當你的類型遵守 Equatable 協(xié)議之后待诅,你同時也確保了具體的實現(xiàn)會遵守該協(xié)議在文檔中列出的語義叹坦。簡單來說,這些語義為:
- 相等意味著可替換性 —— 任何兩個相等的實例都可以在基于實例值的代碼中替換使用卑雁。
- 為了保持可替換性募书,== 運算符應該考慮 Equatable 協(xié)議遵守者中所有可見的成員。這意味著测蹲,如果你編寫了一個 Person 結(jié)構(gòu)體莹捡,它包含 firstName 和 lastName 兩個屬性,那么在實現(xiàn)該結(jié)構(gòu)體的 == 操作符時扣甲,如果你只使用 firstName 判斷相等篮赢,你就違反了協(xié)議的語義。
- a == a 總是 true(反身性); a == b 意味著 b == a(對稱性); a == b 和 b == c 意味著 a == c(傳遞性)琉挖。
- 不等與相等是互斥的启泣,所以如果你自定義了一個 != 操作符(不是必須的),你必須保證 a != b 隱含 !(a == b)示辈。
對類實例的恒等式(===)來說种远,語義基本是同等的。實現(xiàn)上取決于具體類型的特點顽耳。來看看 Jordan Rose 的評論坠敷。
協(xié)議應該使用有意義的語法
再次強調(diào),協(xié)議不只包含語法射富,還有語義膝迎。一層含義是完全沒有語法要求的協(xié)議也是完全符合語法規(guī)范的,比如 MyProtocolWithSpecialSemantics {}胰耗。
“純語義”協(xié)議的一個典型例子是 Error限次,當你給類型添加 Error 的一致性時意味著你告知他人你打算用這個類型進行錯誤報告。比如:extension String: Error { }
稍后
throw "File not found"而另一層含義是類型滿足了協(xié)議的所有要求之后柴灯,Swift 不能自動地使該類型遵守協(xié)議卖漫,因為編譯器無法判斷語義。
協(xié)議的要求 —— 特別是能夠進入標準庫的協(xié)議的要求 —— 定義應該必要且完備赠群,以便可以基于它們實現(xiàn)有價值且通用的算法羊始。
回到 DefaultConstructible 協(xié)議的話題上來,我不認為一個只保證 T() 形式的構(gòu)造器卻沒有任何語義的協(xié)議能實現(xiàn)什么有趣的算法查描。
一個通用的默認值這樣的想法是否有意義呢突委?
或者你可以自問:什么語義可以歸結(jié)到這樣的協(xié)議上柏卤。有價值的算法來自于一組連貫性的語義組成的約束。
通用的 init() 存在的一個問題是匀油,在沒有附加上下文的情況下缘缚,不同的 T 對應的構(gòu)造器 T() 的含義有著巨大的差別:
- 一些類型具有直觀的“空值”表示;這些類型可以很好地匹配無參數(shù)的構(gòu)造器:String() 創(chuàng)建一個空字符串敌蚜;Array()桥滨、Dictionary() 和 Set() 創(chuàng)建一個空的集合。
- 數(shù)字和布爾類型的空值就沒那么清晰了弛车。為什么 Bool() 初始化為 false 而不是 true该园?看起來幾乎是隨意設定的。所有的數(shù)字類型的初始化值都是 0帅韧,直覺上你覺得是由于 0 滿足了 a + 0 == a里初,直到你意識到其實 1 也是一個有效的選擇,因為任意數(shù)字都滿足 a * 1 == a忽舟。
- 此外還有一些不是數(shù)值的對象双妨。例如,UIView() 和 Thread() 每次調(diào)用都會創(chuàng)建不同的對象 —— 盡管這些對象的屬性被設置成了“默認”值叮阅,但是你無法闡述“默認”的 UIView 和 Thread 對象是何種含義刁品。
據(jù)我所知,nil 格式的默認值沒有太多的用處浩姥,除非你能得到有關(guān)默認值的具體信息挑随。 我希望遇到存在默認值的情況時,你可以得到一個比 nil 更有用的默認值勒叠,這就需要提供更多有關(guān)當前類型的知識兜挨,顯然一個寬泛的 DefaultConstructible 是無法提供的。
所以眯分,一些類型擁有有意義的默認值拌汇,而另一些則沒有。你不能為 init() 的概念分配擁有一致性的語義 —— 除非你添加上下文弊决。
RangeReplaceableCollection 的語義
標準庫中已經(jīng)有一個協(xié)議要求實現(xiàn) init()噪舀,它就是:RangeReplaceableCollection。同 DefaultConstructible 不同的是飘诗,在該協(xié)議的上下文中与倡,init() 是有含義的。我們可以得知協(xié)議的遵守者是一個集合昆稿,因此 T() 代表一個“空集合”纺座。還可以斷言 T() 等價于 someCollection.removeAll()。
RangeReplaceableCollection 的上下文對于語義的歸屬是必不可少的貌嫡,上下文指示了這個要求必須在協(xié)議中定義比驻,而不是分解成獨立的協(xié)議(那些細化了 RangeReplaceableCollection 功能的新協(xié)議)。
使用 DefaultConstructible 的話你不知道關(guān)于 T 的值的任何信息岛抄。你無法可靠地使用它别惦。如果默認的構(gòu)造器是一些更大型的協(xié)議(比如 RangeReplaceableCollection)的一部分,那么你就可以說:“它創(chuàng)建了一個空的集合” 以及 “采用默認構(gòu)造器初始化的實例相當于一個實例調(diào)用了 removeAll 方法夫椭〉УВ”這并不意味著將 init() 的含義與協(xié)議脫離。正確的含義是在協(xié)議的遵守者中引入一個 init()蹭秋,該無參構(gòu)造器會在協(xié)議語義所影響的基礎操作中擔任重要的角色扰付。
[...]
將有意義的協(xié)議分割成只有語法價值的塊是有問題的。我懷疑這里有些事情搞錯了仁讨。
分解實現(xiàn)部分是一回事羽莺,這個過程是思考 DRY 的好時機。只有當通用性可以抽象成某種類型的通用代碼時才需要考慮分解需求洞豁。事實上盐固,過程是反過來的 —— 需求聚合時需要創(chuàng)建概念(也就是協(xié)議)—— 這是通用性編程過程的重要部分。
協(xié)議的沖突(相同的語法丈挟,不同的語義)
另一層含義是刁卜,如果兩個協(xié)議的要求具有(部分或完全)相同的語法但語義不同,那么一個類型不應該同時遵守這兩個協(xié)議曙咽,因為沒有辦法同時滿足兩者的語義蛔趴。
Xiaodi Wu:
事實上,我明白語義包含了大量的思考例朱。協(xié)議應該攜帶語義這個概念正在被嚴格地遵守孝情。所以我認為 DefaultConstructible 這個建議的確是有害的,因為它明顯違背了這個重要的思想洒嗤,它的語義只能由人去支持而不是由編譯器去支持咧叭。
沒有語義,協(xié)議就退化成了反射
如果它走起路來像鴨子烁竭,嘎嘎的叫聲像鴨子菲茬,那它可能就是一只鴨子。
—— 鴨子類型的經(jīng)驗法則
如我們所知派撕,鴨子式的類型推斷不是 Swift 中首選的思維方式婉弹,因為一個對象具有的功能(表現(xiàn)為實現(xiàn)了特定的 API)對表達語義沒有任何作用。
Xiaodi Wu 認為通過增強 Swift 對反射機制的支持终吼,將更好地滿足 DefaultConstructible 協(xié)議的目的:
在底層镀赌,如果你想要一種方法來確定一個類型中是否有 init()。個人覺得這聽起來像是反射际跪,而不是協(xié)議的一致性商佛。
(反駁的觀點是:反射不能給你編譯期的安全性喉钢。)
Swift 避免將默認的初始值設置為“零”
反對 DefaultConstructible 協(xié)議的第四點是它與 Swift 的策略沖突,沒有將值初始化為“零”或其他的默認值良姆。與其他許多語言相反肠虽,Swift 不會清除變量的內(nèi)存 —— 編譯器強制開發(fā)者使用顯式的值初始化每個變量。
依據(jù)這種設計哲學玛追,Swift 在處理與 DefaultConstructible 協(xié)議類似的其他語言中同樣存在的算法(比如工廠方法)時會將構(gòu)造默認初始值的職責傳遞給具體的方法調(diào)用者税课。
Tony Allevato 認為,在 Swift 中痊剖,把構(gòu)造器作為一個函數(shù)傳入可以非常優(yōu)雅地實現(xiàn)這個目的:
在編程時韩玩,有幾次我需要提供泛型類型 T 的實例,每當這樣的情況 —— 多虧了 Swift 中的函數(shù)是一級公民并且構(gòu)造器可以作為函數(shù)使用 —— 我都發(fā)現(xiàn)讓工廠方法接受一個 ()->T 類型的參數(shù)并傳入 T.init 要比傳入 T 本身的約束更加清晰陆馁。
本文由 SwiftGG 翻譯組翻譯找颓,已經(jīng)獲得作者翻譯授權(quán),最新文章請訪問 http://swift.gg叮贩。