[译]Swift模块中的API污染

55 阅读4分钟

原文链接 API Pollution in Swift Modules

当您将模块导入到Swift代码中时,您期望结果完全是附加的。也就是说:新功能的潜力是免费的(除了,比如说,你的应用包的大小适度增加)。

导入NaturalLanguage框架,然后Boom应用程序可以**确定文本语言;导入Core Motion,然后whoosh应用程序可以响应设备方向的**更改。但是,如果区分法语和日语的能力干扰了你的应用辨别哪边是磁北的能力,那就令人惊讶了。

尽管这个特殊的例子并不真实(让北海道的法语国家松了一口气),但在某些情况下,Swift依赖会改变你的应用程序的行为方式——即使你不直接使用它

在本周的文章中,我们将研究几种导入模块可以无声地改变现有代码行为的方法,并就如何作为应用编程接口提供商防止这种情况发生以及作为应用编程接口消费者减轻这种影响提供建议。

模块污染

这是一个和<time. h>一样古老的故事:有两件事叫做Foo,编译器必须决定做什么。

几乎每种具有代码重用机制的语言都必须以这样或那样的方式处理命名冲突。在Swift的情况下,您可以使用完全限定的名称来区分模块A(A. Foo)中声明的Foo类型和模块B(B. Foo)中声明的Foo类型。然而,Swift有一些独特的特性,导致编译器没有注意到其他模糊之处,这可能导致导入模块时对现有行为的更改。

运算符过载

在Swift中,当操作数是数组时,+运算符表示串联。一个数组加上另一个数组会导致一个数组,其中前一个数组的元素紧随其后。

let oneTwoThree: [Int] = [1, 2, 3]let fourFiveSix: [Int] = [4, 5, 6]oneTwoThree + fourFiveSix // [1, 2, 3, 4, 5, 6]

如果我们查看**标准**库中操作员的声明,我们会看到它是在数组上的一个不合格扩展中提供的:

extension Array {  @inlinable public static func + (lhs: Array, rhs: Array) -> Array {}}

Swift编译器负责解决对其相应实现的API调用。如果调用匹配多个声明,编译器将选择最特定的可用声明。

为了说明这一点,请考虑Array上的以下条件扩展,它定义了+运算符来对元素符合数字的数组执行成员添加:

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] 😕

因为元素:数字的要求比标准库中的非限定声明更具体,所以Swift编译器将+解析为该函数。

现在,这些新语义学可能完全可以接受——确实更可取。但前提是你知道它们。问题是,如果你导入一个包含这样一个声明的模块,你甚至可以在不知道的情况下改变整个应用的行为。

这个问题不仅限于语义学问题;它也可能是人体工程学的结果。

功能阴影

在Swift中,函数声明可以为尾随参数指定默认参数,使它们对调用方来说是可选的(尽管不一定是可选的)。例如,顶层函数**转储(_: name: indent: max Deep th: max Items:)**具有令人生畏的参数数量:

@discardableResult func dump<T>(_ value: T, name: String? = nil, indent: Int = 0, maxDepth: Int = .max, maxItems: Int = .max) -> T

但是由于默认参数,您只需要指定第一个调用它的参数:

dump("🏭💨") // "🏭💨"

唉,当方法签名重叠时,这种便利的来源可能会成为一个混乱的点。

想象一个假设的模块——不熟悉内置转储函数——定义了一个转储(_:),它打印字符串的UTF-8代码单元。

public func dump(_ string: String) {    print(string.utf8.map {$0})}

在Swift标准库中声明的转储函数在其第一个参数中接受一个不合格的泛型T参数(实际上是任何)。因为String是一种更具体的类型,所以Swift编译器将在可用时选择导入的转储(_:)方法。

dump("🏭💨") // [240, 159, 143, 173, 240, 159, 146, 168]

与前一个例子不同,不完全清楚竞争声明中是否有任何模糊性。毕竟,开发人员有什么理由认为他们的转储(:)方法可能会以任何方式与转储(: name: indent: max Deep th: max Items:)混淆?

这就引出了我们的最后一个例子,这也许是最令人困惑的…

字符串插值污染

在Swift中,可以通过在字符串文字中插值来组合两个字符串,作为连接的替代方法。

let name = "Swift"let greeting = "Hello, \(name)!" // "Hello, Swift!"

从Swift的第一个版本开始就是如此。但是,使用Swift 5中的新**Express By **String Interpolation协议,这种行为不再被视为理所当然。

考虑String的默认内插类型的以下扩展:

extension DefaultStringInterpolation {    public mutating func appendInterpolation<T>(_ value: T) where T: StringProtocol {        self.appendInterpolation(value.uppercased() as TextOutputStreamable)    }}

String Protocol继承了Text Output Streamable和Custom String Converble协议,使其比**Default String Interpolation声明append Interpolation方法更具体,否则在插值String**值时会调用该方法。

public struct DefaultStringInterpolation: StringInterpolationProtocol {    @inlinable public mutating func appendInterpolation<T>(_ value: T)        where T: TextOutputStreamable, T: CustomStringConvertible {}}

斯威夫特编译器的特异性概念再次导致行为从预期变成意外。

如果应用中的任何模块都可以访问上一个声明,它将更改所有内插字符串值的行为。

let greeting = "Hello, \(name)!" // "Hello, SWIFT!"

鉴于语言的快速上升轨迹,期望这些问题在未来的某个时候得到解决并不是不合理的。

但与此同时我们该怎么办?以下是一些关于作为API使用者和API提供者管理此行为的建议。

API消费者的策略

作为一个应用编程接口消费者,您在许多方面都受制于导入依赖项带来的约束。这真的不应该是你需要解决的问题,但至少有一些补救措施可以给你。

向编译器添加提示

通常,让编译器执行您想要的操作的最有效方法是显式地将参数转换为与您想要调用的方法匹配的类型。

以我们之前的转储(_:)方法为例:通过从String向下转换到自定义字符串转换,我们可以让编译器解析调用,转而使用标准库函数。

dump("🏭💨") // [240, 159, 143, 173, 240, 159, 146, 168]dump("🏭💨" as CustomStringConvertible) // "🏭💨"

适用范围的进口声明

Fork依赖

如果一切都失败了,你总能自己解决问题。

如果你不喜欢第三方依赖者正在做的事情,只需分叉源代码,去掉你不想要的东西,然后用它来代替。(你甚至可以尝试让他们在变化的上游。

API提供商的策略

作为开发应用编程接口的人,你最终有责任在设计决策中深思熟虑。当你想到你的行为会带来更大的后果时,以下是一些要记住的事情:

使用泛型约束更加挑剔

不合格的泛型约束与任何相同。如果这样做是有意义的,请考虑使您的约束更加具体,以减少与不相关声明重叠的机会。

从便利性中分离核心功能

一般来说,代码应该被组织成模块,这样模块就负责单一的责任。

如果这样做是有意义的,请考虑将类型和方法提供的功能打包到一个模块中,该模块与您提供给内置类型的任何扩展分开,以提高它们的可用性。在有可能从模块中选择我们想要的行为之前,最好的选择是让消费者选择加入功能,如果它们有可能在下游引起问题的话。

避免碰撞

当然,如果你能从一开始就有意避免碰撞,那就太好了……但是这涉及到整个“未知未知”的事情,我们现在没有时间进入认识论。

所以现在,我们只能说,如果你意识到某件事可能是一场冲突,一个好的选择可能是完全避免它。

例如,如果你担心有人会对改变基本算术运算符的语义学感到愤怒,你可以选择不同的运算符,比如。+:

infix operator .+: AdditionPrecedenceextension 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]

作为开发人员,我们可能不太习惯考虑我们决策的更广泛影响。代码是看不见的,没有重量,所以很容易忘记它甚至在我们运送后就存在了。

但是在斯威夫特,我们的决定产生了超出人们立即理解的影响,所以考虑我们如何履行作为原料药管理者的责任是很重要的。