重学 Block & Closure 系列之 Closure 篇 | Soledad
介绍
Swift 中的 Closure 和 Objective-C 中的 Block 比较相似。Closure 也可以捕获和保存上下文中任意常量和变量的引用(它自己也是一个引用类型)。另外,Swift 会为你管理所有捕获过程中的内存操作。
Closure 也分三种:
- 全局函数(对,函数也是闭包的一种),英文叫做 Global functions,一个有名字并且不会捕获任何值的 Closure
- 嵌套函数,Nested function,它是一个有名字并可以捕获其外部值的闭包。例如:123456789101112func myFunctionWithNumber(_ someNumber: Int) {func increment(_ step: Int) -> Int {return someNumber + 10}let incrementedNumber = increment(someNumber)print("The incremented number is \(incrementedNumber)")}myFunctionWithNumber(5)// The incremented number is 15
在这里,func increment(var someNumber: Int) -> Int 就是一个嵌套函数,它捕获了外部的 someNumber 值。
作为优化,如果被 Closure 捕获的值没有被修改,并且在 Closure 创建完之后同样没有被修改的话,那么 Swift 会捕获它的复制。
- 闭包表达式,Closure expression,一个可以捕获其上下文中变常量值的匿名闭包,通常使用轻量级语法编写
我们先来看一下闭包表达式的完整格式:
1 2 3 | { (parameter) -> return type in statement } |
举例
官方文档中提供了这样一个例子。
1 | let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] |
names 可以调用 sorted(by:) 方法,该方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来
表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。
如果将函数作为参数进行传入的话,是这么做:
1 2 3 4 5 | func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 } var reversedNames = names.sorted(by: backward) // reversedNames 为 ["Ewa", "Daniella", "Chris", "Barry", "Alex"] |
如果使用闭包表达式的方法:
1 2 3 | reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 }) |
关键字 in 将 闭包的参数和返回值类型定义 与 闭包函数体 进行分离操作。
特点
与 Block 相比,Closure 有几个明显的特点。
- 可以通过上下文推断参数和返回值类型
- 单表达式 Closure 可以省略 return 关键字
- 参数名称简写
- 尾闭包特有的语法
接下来,我们举例说明这几点。
根据上下文推断类型
还是拿上面的 sorted(by:) 举例。由于我们的 names 是字符串数组,那么我们排序时的 s1, s2 可以被推断出来肯定是 String。所以我们不需要再写具体的参数类型以及完整的闭包格式:
1 | reversedNames = names.sorted(by: { s1, s2 in return s1 > s2}) |
单表达式 Closure 可以省略 return 关键字
如果 Closure 的 statement 只有一行的话,我们还可以省略 return:
1 | reversedNames = names.sorted(by: { s1, s2 in s1 > s2 }) |
参数名称简写
Swift 还给我们提供了参数简写的方法,用来表明相对应的参数(我们甚至能直接省略 in,因为参数同样是可推断的):
1 | reversedNames = names.sorted(by: { $0 > $1 }) |
尾闭包特有的语法
如果你把一个 Closure 作为一个函数的最后一个参数,我们可以把它写成尾闭包的格式。
1 2 3 4 5 6 7 8 9 | func someFunctionThatTakesAClosure(closure: () -> Void ) { print("hello") closure() } someFunctionThatTakesAClosure { print("my boy") } 打印出 "hello" "my boy" |
对于我们最早的例子,由于 sorted(by:) 同样 Closure 也是最后一个参数,我们可以写成这样:
1 | names.sorted() { $0 < $1 } |
还有,如果该尾闭包是唯一的参数的话,我们可以省略 ():
1 | names.sorted { $0 < $1 } |
关于逃逸闭包(Escaping Closures)
如果一个 Closure 作为参数传给一个函数,但是该 Closure 会在函数返回之后再被调用,那么我们称这样闭包是逃逸闭包。这时需要在闭包参数类型前加 @escaping 关键字来允许该闭包可逃逸。否则编译器就会报错。
来举个例子:
1 2 3 4 | var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) // 该 Closure 一直未被调用,只有到函数结束后才可以被调用 } |
所以说,如果你这样子,那么就可以不加 @escaping :
1 2 3 | func someFunctionWithEscapingClosure(completionHandler: () -> Void) { completionHandler() // 因为你在函数执行过程中就调用了该 Closure,这么说应该明白了吧 } |
在这里, completionHandler 直到该函数执行完成后才会被调用。也就是说,这个闭包会逃逸出该函数。因此必须加上 @escape 标记,否则会编译失败。另外,记住在逃逸闭包里是不会隐式加上 self 的,需要你自己明确的写出来。
关于自动闭包(Autoclosures)
苹果引入了自动闭包以及 @autoclosure 关键字,那么它的作用什么呢?简单的说,它把一句表达式自动地封装成一个 Closure。这样有时在语法上看起来就会非常漂亮。但建议谨慎使用,“如果在容易产生歧义或者误解的时候,还是使用完整的闭包写法会比较好。”
比方说我们接受一个闭包,当闭包执行的结果为 true 的时候进行打印:
1 2 3 4 5 | func logIfTrue(_ predicate: () -> Bool) { if predicate() { print("True") } } |
调用的时候,我们可以这样写:
1 | logIfTrue({return 2 > 1}) //根据闭包简化的特点,我们还可以省略 return。 |
再深入一点,因为这个闭包是最后一个参数,我们可以使用尾闭包的特点,把大括号拿出来然后省略括号,变成:
1 | logIfTrue{ 2 > 1 } |
这样还是有点不太自然,如果我们在参数类型前面加上 @autoclosure 关键字:
1 2 3 4 5 | func logIfTrue(_ predicate: @autoclosure () -> Bool) { if predicate() { print("True") } } |
那么最终我们可以简写成这样:
1 | logIfTrue( 2 > 1 ) // Swift 会**自动**把 2 > 1 这个表达式自动转换为 () -> Bool。 |
最后要提一句的是,@autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能使用这个特性进行简化。
参考
- The Swift Programming Language (Swift 3.1)
- “Swifter - Swift 必备 Tips (第三版).” 王巍 (onevcat).
关注我
- 微博:@CaiYue_
- GitHub: caiyue1993
- 邮箱:yuecai.nju@gmail.com
本文版权所有,如需转载,请告知原作者并注明出处