Swift 的闭包写法很多,但是最正规的应该是完整地将闭包的输入和输出都写上,然后用 in
关键字隔离参数和实现。比如我们想实现一个 Int
的 extension
,使其可以执行闭包若干次,并同时将次数传递到闭包中:
extension Int {
func times(f: Int -> ()) {
print("Int")
for i in 1...self {
f(i)
}
}
}
3.times { (i: Int) -> () in
print(i)
}
// 输出:
// Int
// 1
// 2
// 3
这里闭包接受 Int
输入且没有返回,在这种情况下,我们可以将这个闭包的调用进行简化,成为下面这样:
3.times { i in
print(i)
}
注:在 Xcode 7 beta 6 中这样写会触发一个编译错误:(Any) -> ()' is not compatible with expected type 'Int -> ()',这应该是一个编译器的 bug,或者 Apple 决定以更严格的形式来让开发者使用闭包。我之后会对此继续关注并再次进行更新。
这是我们很常见的写法了,也是比较推荐的写法。但是比如某一天,我们觉得这种传入参数的 times
有些麻烦,很多时候我们并不需要当前的次数,而只是想简单地将一个闭包重复若干次的话,可能我们会写出 Int
的另一个闭包无参数的扩展方法:
extension Int {
func times(f: Void -> Void) {
print("Void")
for i in 1...self {
f()
}
}
}
你也许会这么解读这段代码:Int
有一个扩展方法 times
,它接受一个叫做 f
的闭包,这个闭包不接受参数也没有返回;times
的作用是按照这个 Int
本身的次数来执行 f
闭包若干次。
在早期的 Swift 版本中,这里存在一个歧义调用。虽然在 Swift 1.2 之后的新版本中这个歧义调用问题已经由编译器解决了,但是在修订这个章节时,我认为保留之前的一些讨论可能会对理解整个问题有所帮助。
如果我们在 Swift 1.2 之前的版本中运行这段代码时,输出将发生改变:
// 输出:
// Void
//
//
//
现在的输出变成了 Void 后面接了三行空格。一个以 i
为参数原来正常工作的方法,在加入了一个“不接受参数”的新的方法情况下,却实际上调用了这个新的方法。我们在没有改变原来的代码的情况下,仅仅是加入了新的方法就让原来的代码失效了,这到底是为什么,又发生了什么?
很明显,现在被调用的是 Void
版本的扩展方法。在继续之前,我们需要明确 Swift 中的 Void 到底是什么。在 Swift 的 module 定义中,Void
只是一个 typealias 而已,没什么特别:
typealias Void = ()
那么,()
又是什么呢?在多元组的最后我们指出了,其实 Swift 中任何东西都是放在多元组里的。(42, 42)
是含有两个 Int
类型元素的多元组,(42)
是含有一个 Int
的多元组,那么 ()
是什么?没错,这是一个不含有任何元素的多元组。所以其实我们在 extention 里声明的 func times(f: Void -> Void)
根本不是 “不接受参数” 的闭包,而是一个接受没有任何元素的多元组的闭包。这也不奇怪为什么我们的方法会调用错误了。
当然,在实际使用中这种情况基本是不会发生的。之所以调用到了 Void
版本的方法,是因为我们并没有在调用的时候为编译器提供足够的类型推断信息,因此 Swift 为我们选择了代价最小的 Void
版本来执行。如果我们将调用的代码改为:
3.times { i in
print(i + 1)
}
可以看到,这回的输出是:
// 输出:
// Int
// 2
// 3
// 4
毫无疑问,因为 Void
是没有实现 + 1
的,所以类型推断判定一定会调用到 Int
类型的版本。
其实不止是 Void
,像是在使用多元组时也会有这样的疑惑。比如我们又加入了一个这样看起来是“接受两个参数”的闭包的版本:
extension Int {
func times(f: (Int, Int) -> ()) {
print("Tuple")
for i in 1...self {
f(i, i)
}
}
}
如果我们先注释掉其他的歧义版本,我们可以看到 i in
这种接受一个参数的调用仍然可以编译和运行,它的输出会是:
// Tuple
// (1, 1)
// (2, 2)
// (3, 3)
道理和 Void
是一样的,因此就不再赘述了。
在 Swift 1.2 中,类似上面的有歧义的调用会导致编译器报错,并提醒我们发生歧义的方法。得益于新的编译环境,我们现在可以写出更安全和更有保证的代码。
但无论如何,在使用可能存在歧义的闭包时,过度依赖于类型推断其实是一种比较危险的行为,可读性也很差 -- 除非你自己清楚地知道输入类型,否则很难判断调用的到底是哪个方法。为了增强可读性和安全性,最直接是在调用时尽量指明闭包参数的类型。虽然在写的时候会觉得要多写一些内容,但是在 IDE 的帮助下默认实现也是带有全部参数类型的,所以这并不是问题。相信在之后进行扩展和阅读时我们都会感谢当初将类型写全的决定。
3.times { (i: Int)->() in
print(i)
}
3.times { (i: Void)->() in
print(i)
}
3.times { (i: (Int,Int))->() in
print(i)
}