在 Swift 语言中有很多隐藏“宝藏”悄悄深埋在不为人知的角落,静静等待着有缘秃头码农们的大力挖掘。
而在这里,我们将介绍 Swift 语言中一个非常有用的秘技:方法重载优先级判断以及如何改变它。
相信学完本课后,小伙伴们一定会对 Swift 语言的博大精深所折服,并将本课学到的这一隐秘技能发扬光大!
那还等什么呢?让我们马上开始宝藏的深入挖掘之旅吧!
Let‘s Go!!!
方法重载(function overloading)是很多开发语言都支持的一种特性,它被用来多样化同名方法或函数的调用。使用方法重载可以大大增加同名方法或函数调用的灵活性。
比如,在以下代码中三个 test 函数互为重载:
func test() {
print(#function)
}
func test(value: Int) {
print(#function)
}
func test(value: Int, needChanged: Bool) {
print(#function)
}
方法重载的一般要求为方法名相同,但方法签名会有所不同。值得说明的是,只考虑签名中的返回类型在某些情况下会产生模棱两可的语义。
func test() {
print(#function)
}
func test() -> String {
print(#function)
return "test"
}
/* 直接调用 test() 会产生歧义!
error: Test.playground:13:1: error: ambiguous use of 'test()'
*/
test()
如上代码所示,如果只有方法返回类型不同则会让编译器“不知所措”,我们必须显式告诉编译器我们的“精确”意图:
// 会调用返回 String 版本的 test() 方法
let text: String = test()
注意,不要将方法重载和方法重写(overriding)相混淆:后者被用来支持对象中相同方法的多态性;前者在编译时被考虑,后者则在运行时被考虑。
上面我们了解了 Swift 方法重载的不同形式。那么,对于名称相同的方法来说,如果在调用时满足其中多个重载语义,那么到底实际会使用哪个重载方法呢?
func test() {
print(#function)
}
func test(value: Int = 11) {
print(#function)
}
// 实际会调用上面哪个重载方法呢?
test()
如上例所示,我们创建了名为 test 的两个重载方法,如果我们进行 test() 调用,到底会执行上面哪个方法呢?
前 Swift 核心组成员 Jordan Rose
曾在不经意间透露过解决这种情况的“秘诀”:
The general principle is that the most specific overload should win.
那就是:具体胜过一般原则!
这意味着,当方法的多个重载同时满足调用语义时,越具体的重载越会被优先考虑;约束条件越少的重载越会被优先考虑。
func test() {}
func test(value: Int = 11) {}
还拿上面的例子来说,对于 test() 语义来说,第一个方法会被调用,因为它不需要假设用户选择了默认参数,约束条件更少。
类似的,对于下面的例子,由于不需要假设用户在调用时选择单个参数 test 方法的不定参数重载版本,所以实际 test() 采用的是 test(_ value: Int) 这一约束更少、更简单的重载调用:
func test(_ value: Int) {
print(#function)
}
func test(_ values: Int...) {
print("不定参数(Variadic):\(#function)")
}
// 会调用第一个方法重载
test(11)
同理,如果非要在普通重载和泛型重载中选择一个的话,编译器将会毫不犹豫的选择前者:
func test(_ value: Int) {
print(#function)
}
func test<N: Numeric>(_ value: N) {
print("泛型:\(#function)")
}
// 将会调用非泛型版本的重载
test(11)
俗话说得好:“我命由我不由天”!小伙伴们的疑问是:我们能不能改变编译器对于多个重载版本的选择偏好呢?
答案是肯定的!
在某些情况下,我们希望让本该“不受人待见”的重载方法重新“翻身做主人”。比如,对于之前泛型重载的那个例子,如果我们希望泛型版本的重载优先被调用,该如何是好呢?
func test(_ value: Int) {
print(#function)
}
func test<N: Numeric>(_ value: N) {
print("泛型:\(#function)")
}
// 如果希望调用泛型版本的重载,该如何是好?
test(11)
在 Swift 语言中,幕后秃头大佬们提供了一种机制,让我们可以“降低”某一种方法重载的“偏爱程度”。这可以通过在其方法定义前增加 @_disfavoredOverload
修饰符来搞定:
@_disfavoredOverload
func test(_ value: Int) {
print(#function)
}
func test<N: Numeric>(_ value: N) {
print("泛型:\(#function)")
}
// 现在会调用泛型版本的重载方法
test(11)
如上代码所示,我们向编译器挑明了小伙伴们更“讨厌”非泛型版本重载的强烈情感,所以编译器会非常知趣的选择泛型版本的重载调用。
最后再举一个“栗子”。对于以下 3 种 test() 方法的重载来说,默认会选择第一种重载;当第一种重载被 @_disfavoredOverload 修饰后,才会选择第二种重载;只有当前两种重载都被“嫌弃”之后,才轮到最后的泛型重载闪亮登场:
@_disfavoredOverload
func test(_ value: Int) {
print(#function)
}
@_disfavoredOverload
func test(_ values: Int...) {
print("不定参数(Variadic):\(#function)")
}
func test<N: Numeric>(_ value: N) {
print("泛型:\(#function)")
}
// 只有前两种重载都被“嫌弃”之后,才轮到最后一种重载!
test(11)
现在,小伙伴们对于 Swift 语言中多个重载方法到底该选谁?以及如何进一步“暗箱操作,逆天改命”修改默认重载规则的方法都了然于胸了吧!棒棒哒!
@_disfavoredOverload 修饰符前面有下划线,这意味着它是 Swift 语言的一个内部特性,在将来很可能会被丢弃,所以如果大家依赖于它则需要特别留意。
所幸的是,在最新 Swift 5.9 中 @_disfavoredOverload 仍然可用。
在本篇博文中,我们先是介绍了 Swift 语言中当方法的多个重载同时满足调用语义时编译器的选择之道,并且随后进一步讨论了如何“逆天改命”修改这一默认的选择法则。
感谢观赏,再会!