[译] Swift 模块中的 API 污染-阿里云开发者社区

开发者社区> 开发与运维> 正文

[译] Swift 模块中的 API 污染

简介: 当你将一个模块导入 Swift 代码中时,你希望它们产生的效果是叠加的,也就是说,你不需要什么代价就可以使用新功能,仅仅 app 的大小会增加一点。

原文地址: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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章