[譯] Swift 模塊中的 API 污染

Swift 模塊中的 API 污染

當你將一個模塊導入 Swift 代碼中時,你但願它們產生的效果是疊加的,也就是說,你不須要什麼代價就可使用新功能,僅僅 app 的大小會增長一點。前端

導入 NaturalLanguage 框架,你的 app 就能夠 肯定文本的語言。導入 CoreMotion,你的應用能夠 響應設備方向的變化。可是若是進行語言本地化的功能干擾到手機檢測設備方向的功能,那就太難以想象了。android

雖然這個特殊的例子有點極端,但在某些狀況下,Swift 依賴庫能夠改變你 app 的一些行爲方式,即便你不直接使用它也是如此ios

在本週的文章中,咱們將介紹導入模塊能夠靜默更改現有代碼行爲的幾種方法,並提供當你做爲一個 API 生產者有關如何防止這種狀況的發生以及做爲 API 調用者如何減輕這種狀況帶來的影響的一些建議。git

模塊污染

這是一個和 <time.h> 同樣古老的故事:有兩個東西叫作 Foo,而且編譯器須要決定作什麼。github

幾乎全部具備代碼重用機制的語言都必須以某種方式處理命名衝突。在 Swift 裏,你可使用顯式的聲明來區分模塊 A 中的 Foo 類型(A.Foo)和模塊 B 中的 Foo 類型(B.Foo)。可是,Swift 具備一些獨特的風格會致使編譯器忽視其餘可能存在的歧義,這會致使導入模塊時對現有行爲進行更改。express

在本文中,咱們使用 「污染」 這個術語來描述由導入編譯器未顯現的 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 中:api

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 遵循了 一些協議,其中包括 TextOutputStreamableCustomStringConvertible,使其比 經過 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) // "🏭💨"
複製代碼

範圍導入聲明

上一篇文章 中所述,你可使用 Swift 導入聲明來解決命名衝突。

不幸的是,對模塊中某些 API 的導入範圍目前不會阻止擴展應用於現有類型。也就是說,你不能只導入 adding(_:) 方法而不導入在該模塊中聲明 + 運算符的重載。

Fork 依賴庫

若是全部其餘方法都失敗了,你能夠隨時將問題掌握在本身手中。

若是你對第三方依賴庫不滿意,只需 fork 它的源代碼,而後去除你不想要的東西再使用它。你甚至能夠嘗試讓他們上游作出一些改變。

不幸的是,這種策略不適用於閉源模塊,包括 Apple 的 SDK 中的模塊。「雷達或GTFO」。我想你能夠試試 「Radar or GTFO」

API 提供者的策略

做爲開發 API 的人,你有在設計決策中慎重考慮的最終責任。當你考慮你的操做的影響時,請注意如下事項:

對使用泛型約束更加謹慎

未指定的 <T> 泛型約束與 Any 相同。若是這樣作有意義,請考慮使你的約束更具體,以減小與其餘不相關聲明重疊的可能性。

從便利性中分離核心功能

做爲普適規則,代碼應組成模塊而負責單一的責任。

若是這樣作是有意義的,請考慮模塊中類型和方法提供的打包功能,你須要將該模塊與你爲內置類型提供的任何擴展分開,以提升其可用性。在能夠從模塊中挑選和選擇咱們想要的功能以前,最好的解決方案是讓調用者能夠選擇在可能致使下游問題的狀況下選擇性地加入功能。

Avoid Collisions Altogether徹底避免碰撞

固然,若是你可以知情地避免衝突,那就太棒了...可是這會進入整個 「不知之不知」,咱們如今沒有時間討論認識論。

因此如今讓咱們假設,若是你知道某些事情可能會產生衝突,一個好的選擇是徹底避免使用它。

例如,若是你擔憂某人可能會對你重載基本算術運算符感到不滿,你能夠選擇另外一個,好比 .+

infix operator .+: AdditionPrecedence

extension Array where Element: Numeric {
    static func .+ (lhs: Array, rhs: Array) -> Array {
        return Array(zip(lhs, rhs).map {$0 + $1})
    }
}

oneTwoThree + fourFiveSix // [1, 2, 3, 4, 5, 6]
oneTwoThree .+ fourFiveSix // [5, 7, 9]
複製代碼

做爲開發者,咱們可能不太習慣於考慮咱們決策的深遠影響。代碼是看不見的,沒有重量的,因此很容易忘記它在咱們發佈後忘記它的存在。

可是在 Swift 中,咱們的決策產生的影響超出了人們的直接理解,因此考慮咱們如何履行 API 管理員的責任這一點很是重要。

NSMutableHipster

若是你有其餘問題,歡迎給咱們提 Issuespull requests

這篇文章使用 Swift 5.0.。你能夠在 狀態頁面 上查找全部文章的狀態信息。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索