Swift 的闭包写法很多,但是最正规的应该是完整地将闭包的输入和输出都写上,然后用 in 关键字隔离参数和实现。比如我们想实现一个 Intextension,使其可以执行闭包若干次,并同时将次数传递到闭包中:

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)
}