闭包是功能性自包含模块,可以在代码中被传递和使用。 Swift 中的闭包与 C 和 Objective-C中的 blocks 以及其他一些编程语言中的 lambdas(λ) 比较相似。
闭包可以捕获和存储其所在上下文中任意常量和变量的引用。 这就是所谓的闭合并包裹着这些常量和变量,俗称闭包。Swift会为您管理在捕获过程中涉及到的内存操作。
在 函数 中介绍的全局和嵌套函数实际上也是特殊的闭包,闭包采取如下三种形式之一:
- 全局函数是有名称但不会捕获任何值的闭包。
- 嵌套函数是有名称并且可以从其封闭函数中捕获值的闭包。
- 闭包表达式是用轻量级语法写的没有名称但可以捕获上下文的值。
Swift的闭包表达式拥有简洁的风格,并鼓励在常见场景中以实现语法优化,主要优化如下:
- 利用上下文推断参数和返回值类型
- 单表达式(single-expression)闭包可以省略 return 关键字
- 参数名称简写
- 后置闭包语法
闭包表达式(Closure Expressions)
闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。
排序方法(The Sorted Method)
Swift标准库中提供了一个叫做 sorted(by:) 的方法,对会根据提供的值对已知类型的数组进行排序。方法会返回一个已经排序好的数组,但原始的数组不会改变。
闭包表达式语法(Closure Expression Syntax)
闭包表达式语法有一般形式:
{ (parameters) -> return type in
statements
}
在闭包语法表达式中的参数可以作为 in-out 参数,但是不能有默认值。亦可以使用可变参数。元组也可以作为参数和返回值。
上面的🌰也可以这样写:
根据上下文推断类型(Interring Type From Context)
因为Swift中有根据上下文推断类型,所以上面的例子可以简写成这样:
单表达式闭包省略 return 关键(Implicit Returns from Single-Expression Closures)
因为Swift的这条特性,所以上面的🌰又可以简化成:
参数名称简写(Shorthand Argument Names)
Swift 自动为内联函数提供了参数名称简写功能,您可以直接通过 $0,$1,$2等名字来引用的闭包的参数的值,所以上面的🌰还可以简化:
感觉有点der啊。
运算符函数(Operator Functions)
实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。 Swift的 String 类型定义了关于大于号 (>) 的字符串实现,让其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。 而这正好与 sort 函数的第二个参数需要的函数类型相符合。就是说可以这么写:
后置闭包(Trailing Closures)
如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用后置闭包代替。后置闭包在函数结束的括号后面写,尽管如此,它仍是函数的参数。当使用后置闭包语法的时候,不用写参数标签作为函数调用的一部分:
所以上面的那个🌰就可以这么写:
当后置闭包只需要一个参数的时候,『( )』可以省略:
当闭包非常长以至于不能在一行中进行书写时,后置闭包就变得非常有用。 举例来说,Swift 的 Array 类型有一个 map 方法,其获取一个闭包表达式作为其唯一参数。 数组中的每一个元素调用一次该闭包函数,并返回该元素所映射的值(也可以是不同类型的值)。 具体的映射方式和返回值类型由闭包来指定:
- 这个🌰有点复杂,就是当调用map函数的时候,先将16拿出来进行映射。这个后置闭包的参数就是数组中每个元素,返回值是『翻译』之后的字符串。
- 字典 digitNames 下标后跟着一个叹号 (!),因为字典下标返回一个可选值 (optional value),表明即使该 key不存在也不会查找失败。 在上例中,它保证了 number % 10 可以总是作为一个 digitNames 字典的有效下标 key。 因此叹号可以用于强展开 (force-unwrap) 存储在可选下标项中的 String 类型值。
捕获值(Capturing Values)
闭包可以在其定义的上下文中捕获常量或变量。 即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数体内的函数。 嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。
如果我们单独来看这个函数:
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
一目了然,这个函数并没有传进来任何参数,但是在函数体内访问了 runningTotal 和 amount 变量,这就是因为这个闭包捕获了上下文的值。
由于没有修改 amount 变量,incrementor 实际上捕获并存储了该变量的一个副本,而该副本随着 incrementor 一同被存储。
然而,因为每次调用该函数的时候都会修改 runningTotal 的值,incrementor 捕获了当前 runningTotal 变量的引用,而不是仅仅复制该变量的初始值。捕获一个引用保证了当 makeIncrementor 结束时候并不会消失,也保证了当下一次执行 incrementor 函数时,runningTotal 可以继续增加。
注意:『As an optimization(优化), Swift may instead capture and store a copy of a value if that value is not mutated(突变) by a closure, and if the value is not mutated after the closure is created.』(这句话不知道怎么翻译了。😂)
Swift 同时也处理 runingTotal 变量的内存管理操作,如果不再被 incrementor 函数使用,则会被清除。
如果闭包分配给一个类实例的属性,并且该闭包通过指向该实例或其成员来捕获了该实例,将创建一个在闭包和实例间的强引用环。 Swift 使用捕获列表来打破这种强引用环。
闭包是引用类型(Closures Are Reference Types)
上面的例子中,incrementBySeven 和 incrementByTen 是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。 这是因为函数和闭包都是引用类型。
无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen 指向闭包的引用是一个常量,而并非闭包内容本身。
这也就是说如果将闭包赋值给了两个不同的常量(变量),两个值都会指向同一个闭包:
非逃逸闭包(Nonescaping Closures)
当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。
当你定义接受闭包作为参数的函数时,你可以在类型之前标注 『@noescape』,用来指明这个闭包是不允许“逃逸”出这个函数的。将闭包标注 @noescape 能使编译器知道这个闭包的生命周期,从而可以进行一些比较激进的优化。
但是这里需要提一下的就是:闭包只能在函数体中执行,所以编译器明确的知道运行时的上下文:
这里会报错竟然是因为@noescape 是默认的。在文档中,这段的例子好像默认都是@escape,但是在新的版本中默认是@noescape,不知道是我的理解错误了还是文档的问题。不过问题已经反馈了。