六、闭包 Closures
1. 概述
闭包是一种自包含的代码块(self-contained blocks),它实现某一特定功能,并可以在代码中进行传递和使用。闭包类似于 Objective-C 中的 block。
函数其实是一种特殊的闭包,闭包有以下三种形式:
1)全局函数 Global functions —— 有名字,不捕获任何值
2)嵌套函数 Nested functions —— 有名字,捕获自己作用域里的值 capture values from their enclosing function
3)闭包表达式 Closure expressions —— 没有名字,捕获自己作用域里的值
2. 闭包表达式 Closure Expression
2.1. 引例
我们使用Swift库的排序函数 sorted对一组字符串进行排序:
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"] //待排序数组
func backwards(s1: String, s2: String) -> Bool { // 比较器 return s1 > s2 }
var reversed = sort(names, backwards) // reversed is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
上面的代码使用了函数的形式作为闭包定义比较器,它的类型是 (String, String) -> Bool。
显然,仅仅为了 s1>s2 写一个函数是很不简洁的。
2.2. 闭包表达式语法 Closure Expression Synta
闭包表达式的参数可以是constant、variable和inout,但不能是默认值参数(有默认值的参数)。元组 tuple 可以作为闭包表达式的参数和返回值
使用闭包表达式表示引例中的代码:
var reversed = sorted(names, {(s1: String, s2: String) -> Bool in return s1 > s2 })
我们注意到,上面的内联闭包 inline closure 的参数和返回值与引例中的 backwards 函数是相同的,他们都是 (s1: String, s2: String) -> Bool,不同的是,内联闭包的参数和返值是写在大挂号里面,而不是外面。
in 关键字表示了闭包参数和返回值的结束,闭包体 closure’s body 的开始。
因为闭包体很短,所以也可以这样写:
var reversed = sorted
(names, {(s1: String, s2: String) -> Bool in return s1 > s2})
2.3. 类型推断 Inferring Type From Contex
由于Swift的类型推断(sort 函数的第二个参数期望类型 expected type 是 (String, String) -> Bool,所以可以进行类型推断 ),我们可以省略sort 第二个参数的类型
var reversed = sorted(names, {s1, s2 in return s1 > s2})
2.4. 单表达式闭包省略return关键字 Implicit Returns from Single-Expression Closure
上面的语句,甚至return都可以不写
var reversed = sorted(names, {s1, s2 in s1 > s2})
因为闭包体是 (s1 > s2) ,它返回布尔型,所以省略return也不会引起混淆。
2.5. 速记参数名 Shorthand Argument Name
Swift为内联闭包提供了参数的速记方法,使用 $0, $1, $2 等来速记闭包中的参数值。如果你在闭包中使用速记参数名,就可以省略闭包中的参数表。速记参数名的类型将通过参数的expected type 推导出来。因为仅仅只有闭包体了,所以 in 关键字也能省略。
var reversed = sorted(names, {$0 > $1})
这里,$0 代表闭包中的第一个参数,$1代表闭包中的第二个参数。
2.6. 操作符函数 Operator Functions
由于Swift中的String类型定义了特殊的操作符——大于操作符 (>) ,它实际上是 (String, String) -> Bool 类型的函数,所以可以将它直接传递给sort函数。
var reversed = sorted(names, >)
3. 尾部闭包 Trailing Closures
当把闭包作为函数最后一个参数时,如果这个闭包表达式很长,可以使用尾部闭包。
尾部闭包是写在函数的挂号外面的闭包。
func someFunctionThatTakesAClosure(closure: () -> ()) { // function body goes here
} // 不使用尾部闭包 someFunctionThatTakesAClosure({ // closure's body goes here }) // 使用尾部闭 someFunctionThatTakesAClosure() { // trailing closure's body goes here }
当闭包表达式作为函数的唯一参数,并且这个函数使用尾部闭包来表示时,调用函数的时候,可以不写函数后的挂号 “()”
var reversed = sort(names){
$0 > $1
}
4. 闭包的作用域——值的捕获 Capturing Values
这里以嵌套函数值的捕获为例
4.1. 嵌套函数的值的捕获——即嵌套函数变量的作用域
嵌套函数是写在其他函数体内的函数,它可以使用外部函数(母函数)outer function 的所有参数,并且可以使用母函数中定义的变量和常量。例如:
func makeIncrementor(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementor() -> Int { runningTotal += amount return runningTotal } return incrementor }
makeIncrementor 的返回值是一个函数,类型是 () -> Int。由于子函数 incrementor 没有改变参数 amount 的值,所以子函数使用的实际上是 amount 值的副本。子函数改变了runningTotal 的值,使用的是 runningTotal 的引用。决定是使用副本还是引用是有Swift编译器决定的。
使用 makeIncrementor 函数:
// 定义一个 () -> Int 类型的函数 incrementByTen let incrementByTen = makeIncrementor(forIncrement: 10) // 调用 incrementByTen 函数 incrementByTen() // returns a value of 10 incrementByTen() // returns a value of 20 incrementByTen() // returns a value of 30
如果你创建另一个由 makeIncrementor 创建的函数 incrementBySeven,这个函数会存储自己的变量和参数副本。比如在下面的例子中,incrementBySeven 会获得一个全新的runningTotal 的引用,这个引用与 incrementByTen 中的引用没有任何联系。
let incrementBySeven = makeIncrementor(forIncrement: 7)
incrementBySeven() // returns a value of 7 incrementByTen() // returns a value of 40
5. 闭包是引用类型 Closures Are Reference Types
在上面的例子中,incrementByTen 和 incrementBySeven 都是let定义的常量,但是他们却可以改变 runningTotal 值,因为闭包是引用类型(在上面的例子中,我们可以理解为一个常指针 char * const p,p的指向不能变,p的指向的内容可以变)。无论什么时候你将一个函数/闭包赋值给一个变量/常量,实际上是将这个函数/闭包的引用赋值给了常量/变量。在上面的例子中,incrementByTen 的值是闭包的引用,而不是闭包的内容。例如
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // returns a value of 50