第九章 闭包
闭包是一段自包含的功能,可以被传递或者在代码中使用。Swift 中的闭包与 C 或 Objective-C 中的 Blocks 或者一些其他语言中的 lambdas 很相似。
闭包可以捕获到其所在上下文中的任何常量或变量的引用。就是将常量和变量包裹住,因此称为 “闭包”。Swift 会为你处理捕获过程中所涉及到的内存管理任务。
注意:
如果你不熟悉 “捕获” 的概念也不必担心,在后面的 “捕获值”(Capture Values)这个章节中会进行详细的讲解。
在“函数”这一章中介绍的全局函数和嵌套函数是实际上是闭包的一种特殊情况。闭包有如下三种形式:
1. 全局函数是一个有名称且不捕获任何值的闭包;
2. 嵌套函数是一个有名称且在其包裹函数中可以捕获一些值的闭包;
3. 闭包表达式是用轻量级语法所以写的可以在其上下文中捕获值的闭包;
Swift 闭包表达式的语法风格简洁清晰,并且对于常见场景都有优化使得语法更简洁。这些优化包括:
1. 从上下文推断参数类型和返回值类型;
2. 单表达式的闭包隐式返回;
3. 参数名简写;
4. Trailing 闭包语法
9.1 闭包表达式
前面介绍过的嵌套函数,是一种可以方便地在复杂函数中命名一段自包含代码块的方式。有时在编写无完整定义和命名的类似函数的结构时也很有用,尤其在是处理以一个或多个函数作为参数的函数时。
闭包表达式可以用简洁的语法编写内联闭包。闭包表达式提供了一些语法优化,可以以最简单的方式来编写闭包,并且不丢失准确性。下面的闭包表达式例子通过多次迭代精简 sorted 函数来展示这些语法优化,每一个例子都展示了用更高效的方式来编写相同的功能。
Sorted 函数
Swift 标准库中提供了 sorted 函数,会根据你提供的排序闭包来对已知类型的数组进行排序。当排序完成时,sorted 函数返回一个与原数组大小类型相同、元素已经排列有序的新数组。原数组不会被 sorted 函数修改。
下面的例子中,闭包表达式使用 sorted 函数将一个 String 数组按照字典序逆序方式排序。如下是用于排序的原始数组:
<此处添加代码2.7.1- 1>
sorted 函数接收两个参数:
1. 一个已知类型的数组
2. 一个闭包,接收两个与数组元素类型相同的参数,并且返回一个 Bool 值,代表的是第一个元素是否应该排列在第二个元素前面。
这个例子要排序的是 String 类型的数组,所以闭包的函数类型是 (String, String) -> Bool。
提供闭包的一种方式是编写一个相符类型的函数,并将其作为 sort 函数的第二个参数传入:
<此处添加代码2.7.1- 2>
如果第一个字符串(s1)大于第二个字符串(s2),bcakwards 函数返回 true,表示在排好序的数组中 s1 出现在 s2 之前。对于字符串中的字符,“大于” 的含义是 “按字典序出现在后面”。
这意味着字母 “B” 大于字母 “A”,字符串 “Tom” 大于字符串 “Tim”。这个函数进行字典序逆序排列,比如 “Barry” 或出现在 “Alex” 之前。
但这个书写方式很繁琐,实际上只相当于写了一个单表达式函数 (a > b)。在下面的例子中,使用闭包表达式语法可以更好的构造内联闭包。
闭包表达式语法
闭包表达式的形式通常如下:
<此处添加代码2.7.1- 3>
闭包表达式语法可以使用常量、变量和 inout 形参。但不能供默认值。可以在参数列表的末尾使用可变形参。元组可以作为形参或者返回类型。
下面的例子展示了之前的 backwards 函数的闭包表达式版本:
<此处添加代码2.7.1- 4>
需要注意,内联闭包参数和返回类型声明与 backwards 函数类型声明是相同的。在这两个例子中都是 (s1: String, s2: String) -> Bool 类型。但对于内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外。
闭包的函数体部分由关键字 in 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。
因为这个闭包的函数主体非常短,因此可以改写成一行:
<此处添加代码2.7.1- 5>
这表明 sorted 函数的调用可以保持不变,圆括号内仍然包含了所有参数。只是其中一个参数现在变成了内联闭包。
根据上下文推断类型
因为排序闭包是作为一个参数传入函数的,Swift 可以推断其参数类型和返回值类型。第二个参数是 (String, String) -> Bool 类型的函数,也就是说 String,String 和 Bool 类型不需要写到闭包表达式定义中。 因为所有的类型都可以被正确推断,,返回箭头 (->) 和括号也可以省略:
<此处添加代码2.7.1- 6>
实际上在任何情况下,用内联闭包表达式构造的闭包作为参数传递给函数时,都可以推断出其参数和返回值类型,因此,你几乎不需要写出完整格式来构造内联闭包。
然而,你也可以使用明确的类型,这也是鼓励做法,因为这样可以避免阅读代码时可能存在的歧义。这个排序函数例子中,闭包的目的是很明确的,即排序。并且读者可以放心的假设闭包会处理字符串值,因为它是用于协助对字符串数组排序的。
隐式返回的单表达式闭包
单表达式可以省略 return 关键字来隐式返回结果,就像上面的例子:
<此处添加代码2.7.1- 7>
这里的 sorted 函数的第二个函数类型参数明确了闭包会返回一个 Bool 类型值。 因为闭包函数主体只包含了一个表达式 (s1 > s2),该表达式返回 Bool 类型值,因此这里无歧义并且 return 关键字可以省略。
参数名简写
Swift 自动为内联闭包提供简写参数名,可以通过 $0,$1,$2等等来直接访问闭包参数值。
如果你在闭包表达式中使用这些简写参数名,你可以在闭包定义中忽略对参数的定义,并且参数数量及类型会会根据函数类型自行推断。并且 in 关键字也可以省略,因为闭包表达式完全由其主体构成:
<此处添加代码2.7.1- 8>
这个例子中,$0,$1是闭包中第一个和第二个 String 类型参数的引用。
运算符函数
实际上还有一种更简单的方式来书写上面的闭包表达式。Swift 中的 String 类型定义了接收两个 String 类型参数返回一个 Bool 类型的的大于号 (>) 函数。这完全符合 sorted 函数第二个参数所需要的函数类型。因此,你只需要简单的转入大于号,然后 Swift 会推断出这里你需要的是字符串类型的实现:
<此处添加代码2.7.1- 9>
了解更多关于运算符函数的内容,参见 “运算符” (Operator Functions)。
9.2 Trailing 闭包
如果你需要将一个闭包表达式传递给函数作为最后一个参数,但是闭包表达式太长,这时候可以写成 trailing 闭包来解决。Trainling 闭包是写在函数调用括号外(之后)的闭包表达式:
<此处添加代码2.7.2- 1>
注意:
如果函数仅接收一个闭包表达式参数,并且你将闭包表达式写为 trainling 闭包,可以不用在函数调用时添加一对圆括号。
在前面 “闭包表达式语法” 章节中的 sorted 函数,其字符串排序闭包可以改写为:
<此处添加代码2.7.2-2>
当闭包长到不能在一行代码中书写时,trainling 闭包就非常有用了。比如,Swift 中的 Array 类型有一个 map 函数,仅接收一个闭包表达式作为参数。对于数组中的每一个元素这个闭包都执行一次,并且返回一个与之一一对应的值。对应方法和返回值类型需要闭包去说明。
当对数组中的每一个元素执行完闭包之后,map 函数返回一个包含了所有对应值的数组,这些值的顺序与原始值在原数组中的顺序相同。
下面的例子展示了怎样使用 map 函数及 trainling 闭包来将一个 Int 型数组转换为 String 型数组。数组 [16, 58, 510] 用于生成新的数组 [“OneSix”, “FiveEight”, “FiveOneZero”]:
<此处添加代码2.7.2-3>
上面的代码创建了一个用于将整型数字转换为英文数组的映射字典。同时定义了一个准备用于转换的数组。
现在可以使用 trailing 闭包的方式传递一个闭包表达式给数组的 map 函数,来将 numbers 类型数组转换为 String 类型数组。需要注意的是,调用 numbers.map 函数并不需要在 map 后面添加圆括号,因为 map 方法只需要一个参数,并且这个参数以 trainling 闭包方式提供:
<此处添加代码2.7.2-4>
map 函数对数组中的每一个元素都调用了闭包表达式。 不需要指定闭包的输入参数为 number 类型,Swift 能自动根据数组元素推断其类型。
在这个例子中,闭包的 number 参数被声明为一个变量参数,参见“常量和变量形参”(Constant and Variable Parameters),因此可以在闭包函数体内对其进行修改。 闭包表达式指定了返回值类型为 String,用来说明用于返回的数组类型。
闭包表达式在每次被调时创建一个字符串并返回。它使用求余运算符 (number % 10) 来计算最后一位数字,并在 digitNames 字典中寻找映射字符串。这个闭包可以用来生成一个大于 0 整数的字符串代表。
注意:
访问字典 digitNames 的下标后跟着一个叹号 (!),因为字典下标返回一个可选值 (optional value),用来表明当 key 不存在时会查找失败。 在上例中,number % 10 保证了得到的总是 对于 digitNames 字典的有效下标。 因此叹号可以用于强展开 (force-unwrap) 存储在可选下标项中的 String 类型值。
从 digitNames 字典中取回的字符串被加到 output 的前面,导致的结果是创建的字符串数组与原来的整型数组反序。(表达式 number % 10 对于值 16 返回6,对于 58 返回 8,对于 510 返回 0。)
之后 number 变量被除以10。因为它是一个整型,在除运算过程中向下取整,所以 16 变成 1,58 变成 5,510 变成 51。
这个过程一直持续直到 number / 10 等于 0,这时候 output 字符串被闭包返回,然后加入到 map 函数用于返回的数组中。
上面例子中, trainling 闭包整齐地将闭包的功能紧密封装到函数的后面,而不需要将整个闭包包裹到 map 函数的一对圆括号中。
9.3 捕获值
一个闭包可以在其定义的上下文中捕获常量和变量。闭包能在其主体内引用并且修改这些常量和变量值,即使这些常量和变量在其原定义域内已经不存在。
在 Swift 中闭包最简单的形式是写另一个函数主体中的嵌套函数。嵌套函数可以捕获其包裹函数的参数以及任何在包裹函数中定义的常量和变量。
如下例子中 makeIncrementor 函数包含一个名为 incrementor 的嵌套函数。incrementor 嵌套函数从其上下文中捕获两个值 runningTotal 和 amount。捕获到这两个值后,makeIncrementor 函数返回一个闭包 incrementor,这个闭包每次被调用时会让 runningTotal 值增加 amount:
<此处添加代码2.7.3- 1>
makeIncrementor 函数的返回类型是 () -> Int。这表示它返回一个函数,而不是一个简单的值。被返回的这个函数不接收参数,并且返回一个 Int 类型值。想了解更多函数如何返回其他函数,参见 “作为返回类型的函数类型” 这个章节。
makeIncrementor 函数定义了一个名为 runningTotal 的整型变量,来保存用于返回的当前计数值。这个变量的初值为 0。
makeIncrementor 函数仅有一个外部名为 forIncrement 内部名为 amount 的 Int 类型形参。传入的形参对应的实参值指明了 runningTotal 每次增加多少。
makeIncrementor 定义了一个名为 incrementor 的嵌套函数,有它来执行实际的增加操作。这个函数简单的把 amount 加到 runningTotal,并返回结果。
单独来看,incrementor 函数看起来有一些特殊:
<此处添加代码2.7.3- 2>
incrementor 函数没有任何形参,然而在函数体中仍然引用到 runningTotal 和 amount。它通过捕获在上下文环境中存在的 runningTotal 和 amount 值来实现。
因为它并没有修改 amount 的值,increment 实际上在 amount 中存储了值的拷贝。这个值着随着新的 incrementor 函数被存储。
但是,因为它每次被调用的时候都修改了 runningTotal,incrmentor 捕获了一个当前 runningTotal 变量的引用,不仅仅只是值的拷贝。捕获引用可以确保 makeIncrementor 函数结束时 runningTotal 变量不会随之消失,同时确保了 runningTotal 值在下一次被调用的时候仍可用。
注意:
Swift 来决定哪些值是捕获拷贝哪些值是捕获引用。你不需要为 amount 和 runningTotal 添加注解来说明这两个值将用于嵌套函数中。Swift 也负责包括 runningTotal 的释放在内的所有内存管理任务。
下面是一个使用 makeIncrementor 函数的例子:
<此处添加代码2.7.3- 3>
该例子定义了一个叫做 incrementByTen 的常量,该常量指向一个每次调用会将 runningTotal 值加 10 的 incrementor 函数。多次调用这个函数的结果如下:
<此处添加代码2.7.3- 4>
如果你创建了另一个 incrementor,它会有自己独立的 runningTotal 变量引用。下面的例子中,incrementBySevne 捕获了一个新的 runningTotal 变量,该变量和 incrementByTen 中捕获的变量没有联系:
<此处添加代码2.7.3- 5>
注意:
如果将闭包分配给一个类实例的属性, 并且指向该实例或其成员来捕获该实例, 这会导致闭包和实例间的强循环引用。Swift 使用捕获列表来打破这种强循环引用。更多信息,,参见 “强循环引用” (Strong ReferenceCycles for Closures)。
9.4 闭包是引用类型
在上面的例子中,incrementBySeven 和 incrementByTen 是常量,但引用了这些常量的闭包仍然可以将他们捕捉到的变量 runningTotal 值增加。这是因为函数和闭包是引用类型。
无论什么时候,当你把函数或闭包赋值给一个常量或变量时,实际上是把常量或变量设置为函数或闭包的引用。在上面的例子中,incrementByTen 是一个闭包的引用,而非闭包本身。
这也意味着,如果你将一个闭包赋值给两个不同的常量或变量,这两个常量或变量将指向同一个闭包:
<此处添加代码2.7.4- 1>
网友评论