将模块导入Swift代码时,您希望结果完全是加法的。也就是说:新功能的潜力是免费的(除了,应用程序包的大小适度增加)。
导入框架,****繁荣****您的应用程序 可以确定文本的语言 ; 导入和**** whoosh ****您的应用可以 响应设备方向的变化。但是,如果区分法语和日语的能力干扰了你的应用程序能够分辨哪种方式是磁北方,那就太令人惊讶了。Natural<wbr style="box-sizing: border-box;">Language
Core<wbr style="box-sizing: border-box;">Motion
虽然这个特殊的例子并不真实(为了缓解北海道的法语国家),但在某些情况下,Swift依赖可以改变你的应用程序的行为方式 - 即使你不直接使用它。
在本周的文章中,我们将介绍导入模块可以静默更改现有代码行为的几种方法,并提供有关如何防止这种情况作为API提供程序发生的建议,并减轻其作为API使用者的影响。
小编推荐一个群 691040931 里面有许多的iOS开发者在交流技术分享自己的心得,更有一些资料不定期的分享更新。
模块污染
这是一个古老的故事<time.h>
:调用两件事Foo
,编译器必须决定做什么。
几乎所有具有代码重用机制的语言都必须以某种方式处理<dfn style="box-sizing: border-box;">命名冲突</dfn>。在斯威夫特的情况下,你可以使用<dfn style="box-sizing: border-box;">完全限定名称</dfn>的区分Foo
模块声明的类型A
(A.Foo
从)Foo
模块类型B
(B.Foo
)。但是,Swift具有一些独特的特性,导致编译器忽视其他歧义,这可能导致导入模块时对现有行为的更改。
出于本文的目的,我们使用术语“ <dfn style="box-sizing: border-box;">污染”</dfn> 来描述通过导入未由编译器浮出的Swift模块而导致的这种副作用。我们不是100%使用这个术语,所以 如果您能想到任何其他建议,请与我们联系。
运算符重载
在Swift中,+
运算符表示 其操作数为数组时的<dfn style="box-sizing: border-box;">连接</dfn>。一个数组加上另一个数组产生一个数组,其中前一个数组的元素后跟后一个数组。
let oneTwoThree: [Int] = [1, 2, 3]
let fourFiveSix: [Int] = [4, 5, 6]
oneTwoThree + fourFiveSix // [1, 2, 3, 4, 5, 6]
如果我们查看标准库中的运算符 声明,我们会看到它是在一个不合格的扩展中提供的Array
:
extension Array {
@inlinable public static func + (lhs: Array, rhs: Array) -> Array {}
}
Swift编译器负责解析对其相应实现的API调用。如果调用与多个声明匹配,则编译器会选择最具体的声明。
为了说明这一点,请考虑以下条件扩展Array
,它定义了+
运算符,以便 对元素符合以下条件的数组执行<dfn style="box-sizing: border-box;">成员方式添加</dfn>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] 😕
因为要求Element: Numeric
比标准库中的非限定声明更具体,所以Swift编译器解析+
为此函数。
现在,这些新的语义可能是完全可以接受的 - 确实更可取。但只有你知道它们。问题是,如果您导入包含此类声明的模块,您可以在不知情的情况下更改整个应用程序的行为。
这个问题不仅限于语义问题; 它也可以通过符合人体工程学的能力来实现。
功能阴影
在Swift中,函数声明可以为尾随参数指定默认参数,使它们Optional
对于调用者来说是可选的(尽管不一定)。例如,顶级函数 有一个令人生畏的参数:dump(_:name:indent:max<wbr style="box-sizing: border-box;">Depth:max<wbr style="box-sizing: border-box;">Items:)
@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})
}
dump
在Swift标准库中声明的函数T
在其第一个参数(实际上Any
)中采用了非限定泛型参数。因为String
是一种更具体的类型,Swift编译器会在dump(_:)
可用时选择导入的方法。
dump("🏭💨") // [240, 159, 143, 173, 240, 159, 146, 168]
与前面的例子不同,竞争声明中存在任何歧义并不完全清楚。毕竟,开发人员有什么理由认为他们的dump(_:)
方法可能会以任何方式混淆?dump(_:name:indent:max<wbr style="box-sizing: border-box;">Depth:max<wbr style="box-sizing: border-box;">Items:)
这引出了我们最后的例子,这可能是最令人困惑的......
字符串插值污染
在Swift中,您可以通过在字符串文字中插值来组合两个字符串,作为连接的替代方法。
let name = "Swift"
let greeting = "Hello, \(name)!" // "Hello, Swift!"
从Swift的第一个版本开始就是如此。但是,使用 Swift 5中的新 协议,这种行为不再被视为理所当然。Expressible<wbr style="box-sizing: border-box;">By<wbr style="box-sizing: border-box;">String<wbr style="box-sizing: border-box;">Interpolation
考虑以下默认插值类型的扩展String
:
extension DefaultStringInterpolation {
public mutating func appendInterpolation<T>(_ value: T) where T: StringProtocol {
self.appendInterpolation(value.uppercased() as TextOutputStreamable)
}
}
String<wbr style="box-sizing: border-box;">Protocol
继承, 除其他事项外 的和协议,使得它比更具体 的申报方法 是插值时,否则将被调用的值。Text<wbr style="box-sizing: border-box;">Output<wbr style="box-sizing: border-box;">Streamable``Custom<wbr style="box-sizing: border-box;">String<wbr style="box-sizing: border-box;">Convertible
append<wbr style="box-sizing: border-box;">Interpolation``Default<wbr style="box-sizing: border-box;">String<wbr style="box-sizing: border-box;">Interpolation
String
public struct DefaultStringInterpolation: StringInterpolationProtocol {
@inlinable public mutating func appendInterpolation<T>(_ value: T)
where T: TextOutputStreamable, T: CustomStringConvertible {}
}
再一次,Swift编译器的特异性概念导致行为从预期变为意外。
如果应用程序中的任何模块都可以访问上一个声明,则会更改所有插值字符串值的行为。
let greeting = "Hello, \(name)!" // "Hello, SWIFT!"
不可否认,这最后一个例子有点做作; 实现者必须竭尽全力使实现不是递归的。但请考虑这是一个不太明显的例子的替身,这个例子更可能真实地发生在现实生活中。
鉴于语言的快速上升轨迹,期望这些问题在未来的某个时刻得到解决并非没有道理。
但是在此期间我们要做什么呢?以下是作为API使用者和API提供者管理此行为的一些建议。
API消费者的策略
作为API使用者,您在很多方面都会受到导入依赖项所施加的约束。这真的不应该是你要解决的问题,但至少有一些补救措施可供你使用。
添加提示到编译器
通常,让编译器按照您的意愿执行操作的最有效方法是将参数显式地转换为与您要调用的方法匹配的类型。
以我们dump(_:)
之前的方法为例:通过向下转换为from ,我们可以让编译器解析调用以使用标准库函数。Custom<wbr style="box-sizing: border-box;">String<wbr style="box-sizing: border-box;">Convertible``String
dump("🏭💨") // [240, 159, 143, 173, 240, 159, 146, 168]
dump("🏭💨" as CustomStringConvertible) // "🏭💨"
范围导入声明
如前一篇文章中所述, 您可以使用Swift导入声明来解决命名冲突。
不幸的是,对模块中某些API的导入范围目前不会阻止扩展应用于现有类型。也就是说,如果不导入在该模块中声明adding(_:)
的重载+
运算符,则无法导入方法。
Fork依赖关系
如果所有其他方法都失败了,您可以随时将问题掌握在自己手中。
如果您不喜欢第三方依赖项所做的事情,只需分叉源代码,摆脱您不想要的东西,然后使用它。(你甚至可以尝试让他们上游改变。)
不幸的是,这种策略不适用于封闭源模块,包括Apple的SDK中的模块。 我想,“雷达或GTFO”。
API提供商的策略
作为开发API的人,最终您有责任在设计决策中慎重考虑。当您考虑行动的更大后果时,请注意以下事项:
通用约束更加敏锐
不合格的<T>
通用约束与Any
。如果这样做有意义,请考虑使您的约束更具体,以减少与不相关声明重叠的可能性。
从便利性中分离核心功能
作为一般规则,代码应组织成模块,以便模块负责单一责任。
如果这样做是有意义的,请考虑模块中类型和方法提供的打包功能,该模块与您为内置类型提供的任何扩展分开,以提高其可用性。在可以从模块中挑选和选择我们想要的行为之前,最好的选择是让消费者选择在可能导致下游问题的情况下选择加入功能。
完全避免碰撞
当然,如果你能够有意识地避免碰撞开始......那就太棒了......但这会进入整个 “未知未知”的事情,而我们现在没有时间进入认识论。
所以现在,我们只是说如果你知道某些事情可能是冲突,那么一个好的选择可能就是完全避免冲突。
例如,如果您担心某人可能会因改变基本算术运算符的语义而变得烦躁,您可以选择另一个,例如.+
:
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管理员的责任这一点非常重要。
网友评论