原文位址:API Pollution in Swift Modules
原文作者:Mattt
譯文出自:掘金翻譯計劃
本文永久連結:github.com/xitu/gold-m…
譯者:iWeslie
當你将一個子產品導入 Swift 代碼中時,你希望它們産生的效果是疊加的,也就是說,你不需要什麼代價就可以使用新功能,僅僅 app 的大小會增加一點。
導入 NaturalLanguage 架構,你的 app 就可以 确定文本的語言。導入 CoreMotion,你的應用可以 響應裝置方向的變化。但是如果進行語言本地化的功能幹擾到手機檢測裝置方向的功能,那就太不可思議了。
雖然這個特殊的例子有點極端,但在某些情況下,Swift 依賴庫可以改變你 app 的一些行為方式,即使你不直接使用它也是如此。
在本周的文章中,我們将介紹導入子產品可以靜默更改現有代碼行為的幾種方法,并提供當你作為一個 API 生産者有關如何防止這種情況的發生以及作為 API 調用者如何減輕這種情況帶來的影響的一些建議。
子產品污染
這是一個和 一樣古老的故事:有兩個東西叫做 Foo,并且編譯器需要決定做什麼。
幾乎所有具有代碼重用機制的語言都必須以某種方式處理命名沖突。在 Swift 裡,你可以使用顯式的聲明來區分子產品 A 中的 Foo 類型(A.Foo)和子產品 B 中的 Foo 類型(B.Foo)。但是,Swift 具有一些獨特的風格會導緻編譯器忽視其他可能存在的歧義,這會導緻導入子產品時對現有行為進行更改。
在本文中,我們使用 “污染” 這個術語來描述由導入編譯器未顯現的 Swift 子產品引起的這種副作用。我們并不完全承認這個術語,是以如果你有其他更好的任何建議,請 聯系我們。
運算符重載
在 Swift 裡,+ 運算符表示兩個數組連接配接。一個數組加上另一個數組産生一個新數組,其中前一個數組的元素後面跟着後一個數組的元素。
let oneTwoThree: [Int] = [1, 2, 3]
let fourFiveSix: [Int] = [4, 5, 6]
oneTwoThree + fourFiveSix // [1, 2, 3, 4, 5, 6]
如果我們檢視運算符在 标準庫中的聲明,我們可以看到它已經提供了在 Array 的 extension 中:
extension Array {
@inlinable public static func + (lhs: Array, rhs: Array) -> Array {...}
}
Swift 編譯器負責解析對其相應實作的 API 調用。如果調用與多個聲明比對,則編譯器會選擇最具體的聲明。
為了闡釋這一點,請考慮在 Array 上使用以下條件擴充,它定義了 + 運算符,以便對元素遵循 Numeric 的數組執行加法運算:
extension Array where Element: Numeric {
public static func + (lhs: Array, rhs: Array) -> Array {
return Array(zip(lhs, rhs).map {$0 + $1})
}
}
oneTwoThree + fourFiveSix // [5, 7, 9]
因為 extension 中 Element: Numeric 規定了數組元素必須為數字,這比标準庫裡沒有進行顯示的聲明更加具體,是以 Swift 編譯器在遇到元素為數字的數組時會将 + 解析為我們定義的以上函數。
現在這些新語義也許可以接受的,确實它們更加可取,但得在你知道它們怎麼用的時候才行。問題是如果你像 import 一樣導入這樣一個子產品,你可以在不知情的情況下改變整個應用程式的行為。
然而這個問題不僅局限于語義問題。
函數的陰影
在 Swift 中,函數聲明時可以為參數指定預設值,使這些參數在調用時也可以不傳入值。例如,top-level 下的函數 dump(_:name:indent:maxDepth:maxItems:) 有特别多的參數:
@discardableResult func dump<T>(_ value: T, name: String? = nil, indent: Int = 0, maxDepth: Int = .max, maxItems: Int = .max) -> T
但是多虧了參數預設值,你隻需要在調用的時候指定第一個參數:
dump("") // ""
可是當方法簽名重疊時,這種便利來源可能會變得比較混亂。
假設我們有一個子產品,你并不熟悉内置的 dump 函數,是以定義了一個 dump(_:) 來列印字元串的 UTF-8 代碼單元。
public func dump(_ string: String) {
print(string.utf8.map {$0})
}
在 Swift 标準庫中聲明的 dump 函數在其第一個參數(實際上是“Any”)中采用了一個泛型 T 參數。因為 String 是一個更具體的類型,是以當有更具體的函數聲明時,Swift 編譯器将會選擇我們自己的 dump(_:) 方法。
dump("") // [240, 159, 143, 173, 240, 159, 146, 168]
與前面的例子不同的是,與之競争的聲明中存在任何歧義并不完全清楚。畢竟開發人員有什麼理由認為他們的 dump(_:) 方法可能會以任何方式與
dump(_:name:indent:maxDepth:maxItems:) 相混淆呢?
這引出了我們最後的例子,它可能是最令人困惑的...
字元串插值污染
在 Swift 中,你可以通過在字元串文字中的插值來拼接兩個字元串,作為級聯的替代方法。
let name = "Swift"
let greeting = "Hello, \(name)!" // "Hello, Swift!"
從 Swift 的第一個版本開始就是如此。自從 Swift 5 中新的 ExpressibleByStringInterpolation 協定的到來,這種行為不再是理所當然的。
考慮 String 的預設插值類型的以下擴充:
extension DefaultStringInterpolation {
public mutating func appendInterpolation<T>(_ value: T) where T: StringProtocol {
self.appendInterpolation(value.uppercased() as TextOutputStreamable)
}
}
StringProtocol 遵循了 一些協定,其中包括 TextOutputStreamable 和 CustomStringConvertible,使其比 通過 DefaultStringInterpolation 聲明的 appendInterpolation 方法 更加具體,如果沒有聲明,插入 String 值的時候就會調用它們。
public struct DefaultStringInterpolation: StringInterpolationProtocol {
@inlinable public mutating func appendInterpolation<T>(_ value: T) where T: TextOutputStreamable, T: CustomStringConvertible {...}
}
再一次地,Swift 編譯器的特異性導緻我們預期的行為變得不可控。
如果 app 中的任何子產品都可以跨越通路以前别子產品中的聲明,這就會更改所有插值字元串值的行為。
let greeting = "Hello, \(name)!" // "Hello, SWIFT!"
不可否認,這最後一個例子有點做作,實作這個函數時必須盡全力確定其實非遞歸。但請注意這是一個不明顯的例子,這個例子更可能真實地發生在現實應用場景中。
鑒于語言的快速疊代,期望這些問題在未來的某個時刻得到解決并非沒有道理。
但是在此期間我們要做什麼呢?以下是作為 API 使用者和 API 提供者管理此行為的一些建議。
API 使用者的政策
作為 API 使用者,你在很多方面都會受到導入依賴項所施加的限制。它确實 不應該 是你要解決的問題,但至少有一些補救措施可供你使用。
向編譯器添加提示
通常,讓編譯器按照你的意願執行操作的最有效方法是将參數顯式地轉換為與你要調用的方法比對的類型。
以我們之前的 dump(_:) 方法為例:通過從 String 向下轉換為 CustomStringConvertible,我們可以讓編譯器解析調用以使用标準庫函數。
dump("") // [240, 159, 143, 173, 240, 159, 146, 168]
dump("" as CustomStringConvertible) // ""
作者:掘金翻譯計劃
連結:
https://juejin.im/post/5cb80f1f6fb9a0686a224a18來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。