2.7 闭包

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/closures/

2.7 闭包

把一起执行的代码组合起来,而不必创建有名字的函数。

闭包是可以四处传递、在代码中使用的一段独立功能。Swift 中的闭包与其他编程语言中的闭包、匿名函数、lambda 和代码块相似。

闭包可以捕获并保存它所定义所处上下文中任何常量和变量的引用。这称为闭包捕获(closing over)这些常量和变量。捕获所需的所有内存管理都由 Swift 替你完成。

注意:如果你不熟悉捕获这个概念,不必担心。 下文的捕获值中有详细解释。

函数中介绍的全局函数和嵌套函数,其实都是闭包的特殊情况。闭包有三种形式:

  • 全局函数是有名字、不捕获任何值的闭包。
  • 嵌套函数是有名字、可以捕获外层函数中值的闭包。
  • 闭包表达式是用轻量语法书写的无名闭包,可以捕获周围上下文中的值。

Swift 的闭包表达式风格干净清晰,并带有若干优化,鼓励在常见场景中使用简短、不啰嗦的语法。这些优化包括:

  • 从上下文推断参数类型和返回值类型
  • 单表达式闭包的隐式返回
  • 简写实参名
  • 尾随闭包语法

闭包表达式

嵌套函数中介绍的嵌套函数,是把独立的代码块作为更大函数的一部分来命名和定义的一种便利方式。不过,有时我们想写的是更简短的、类似函数的构造,而不需要完整的声明和名字。当你使用那些把一个或多个函数作为实参的函数或方法时,这一点尤其明显。

闭包表达式是用简短、聚焦的语法书写内联闭包的一种方式。闭包表达式提供了若干语法优化,让你能用缩短的形式书写闭包,而不损失清晰度与意图。下面这些闭包表达式示例通过多次改进同一个 sorted(by:) 方法的例子,展示了这些优化,每次都用更简洁的方式表达相同的功能。

sorted 方法

Swift 标准库提供了一个名为 sorted(by:) 的方法,它根据你提供的排序闭包的输出,对已知类型的数组进行排序。排序完成后,sorted(by:) 方法返回一个与旧数组类型和大小相同的新数组,其元素已经按正确的顺序排好。原数组不会被 sorted(by:) 方法修改。

下面的闭包表达式示例用 sorted(by:) 方法把一个 String 值数组按字母逆序排序。下面是要排序的初始数组:

1
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:) 方法接受一个闭包,该闭包接收两个与数组元素类型相同的实参,并返回一个 Bool 值,说明排序后第一个值应当排在第二个值之前还是之后。如果第一个值应当排在第二个值之前,排序闭包需要返回 true,否则返回 false。

这个例子是对 String 值数组排序,因此排序闭包必须是一个 (String, String) -> Bool 类型的函数。

提供排序闭包的一种方式是写一个类型正确的普通函数,并把它作为实参传给 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"]

如果第一个字符串(s1)大于第二个字符串(s2),backward(_:_:) 函数就返回 true,表示 s1 应当排在排序后数组中的 s2 之前。对于字符串中的字符,“大于"意味着"在字母表中更靠后”。也就是说,字母 "B"“大于"字母 "A",字符串 "Tom" 大于字符串 "Tim"。这得到的就是字母逆序排列,"Barry" 被排在 "Alex" 之前,以此类推。

不过,对一个本质上只有单个表达式(a > b)的函数来说,这种写法太啰嗦了。在这个例子里,用闭包表达式语法把排序闭包内联写出来会更好。

闭包表达式语法

闭包表达式语法的一般形式如下:

1
2
3
{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

闭包表达式语法中的参数可以是输入输出参数,但不能有默认值。如果给可变参数起了名字,也可以使用可变参数。元组同样可以用作参数类型和返回类型。

下面的例子展示了上面那个 backward(_:_:) 函数的闭包表达式版本:

1
2
3
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

注意,这个内联闭包的参数声明和返回类型声明与 backward(_:_:) 函数完全相同,都写作 (s1: String, s2: String) -> Bool。不过对于内联闭包表达式来说,参数和返回类型写在花括号内部,而不是外部。

闭包体的开始由 in 关键字引出。这个关键字表示闭包的参数与返回类型的定义已经结束,闭包体即将开始。

由于这个闭包的语句体很短,甚至可以写在一行里:

1
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

这说明对 sorted(by:) 方法的整体调用保持不变:仍然用一对圆括号把该方法的整个实参包起来,只不过这个实参现在是一个内联闭包。

从上下文推断类型

由于排序闭包是作为实参传给某个方法的,Swift 可以推断出它的参数类型和返回值类型。sorted(by:) 方法是在字符串数组上调用的,因此它的实参必须是 (String, String) -> Bool 类型的函数。这意味着闭包表达式的定义中不必写出 (String, String) 和 Bool 类型。由于所有类型都可以推断出来,返回箭头(->)和参数名两边的圆括号也可以省略:

1
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

把闭包作为内联闭包表达式传给函数或方法时,总是可以推断出参数类型和返回类型。因此,当闭包用作函数或方法的实参时,你永远不需要写出它最完整的形式。

尽管如此,如果你愿意,仍然可以显式写出类型;如果这样能避免代码读者产生歧义,也是值得鼓励的。对于 sorted(by:) 方法来说,由于正在进行排序,闭包的用途很清楚,读者也可以放心地假设这个闭包处理的很可能是 String 值,因为它在协助排序一个字符串数组。

单表达式闭包的隐式返回

单表达式闭包可以在声明中省略 return 关键字,从而隐式返回其唯一表达式的结果,如下面这个上一例子的新版本:

1
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

这里,sorted(by:) 方法实参的函数类型清楚地表明闭包必须返回 Bool 值。由于闭包的语句体只包含一个返回 Bool 值的表达式(s1 > s2),不存在歧义,因此可以省略 return 关键字。

简写实参名

Swift 会为内联闭包自动提供简写实参名,可以用 $0、$1、$2 等名字引用闭包实参的值。

如果在闭包表达式中使用这些简写实参名,就可以在定义中省略闭包的实参列表。简写实参名的类型由期望的函数类型推断而来,而你所用的编号最大的简写实参决定了闭包接受多少个实参。in 关键字也可以省略,因为闭包表达式完全由它的语句体构成:

1
reversedNames = names.sorted(by: { $0 > $1 } )

这里 $0 和 $1 指闭包的第一个和第二个 String 实参。由于 $1 是编号最大的简写实参,这个闭包就被理解为接受两个实参。又因为这里的 sorted(by:) 函数期望一个两个实参都是字符串的闭包,所以简写实参 $0 和 $1 都是 String 类型。

运算符方法

其实还有一种更短的写法来表达上面的闭包表达式。Swift 的 String 类型为大于运算符(>)定义了字符串专用的实现,它是一个有两个 String 类型参数、返回 Bool 类型值的方法。这正好与 sorted(by:) 方法所需的方法类型吻合。因此,你只需传入大于运算符,Swift 就会推断你想使用它针对字符串的实现:

1
reversedNames = names.sorted(by: >)

关于运算符方法的更多内容,参见运算符方法。

尾随闭包

如果你需要把一个闭包表达式作为函数的最后一个实参传入,而该闭包表达式又很长,那么把它写成尾随闭包可能会很有用。尾随闭包写在函数调用圆括号的后面,尽管它仍然是函数的一个实参。使用尾随闭包语法时,函数调用中不写第一个闭包对应的实参标签。一个函数调用可以包含多个尾随闭包;不过下面的前几个例子只使用一个尾随闭包。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体写在这里
}

// 不使用尾随闭包调用这个函数:

someFunctionThatTakesAClosure(closure: {
    // 闭包体写在这里
})

// 改用尾随闭包调用这个函数:

someFunctionThatTakesAClosure() {
    // 尾随闭包的闭包体写在这里
}

上面闭包表达式语法一节中的字符串排序闭包,可以写在 sorted(by:) 方法的圆括号之外,作为尾随闭包:

1
reversedNames = names.sorted() { $0 > $1 }

如果闭包表达式是函数或方法唯一的实参,而且你把它写成尾随闭包,那么调用该函数时就不必在函数或方法名后面写一对圆括号 ():

1
reversedNames = names.sorted { $0 > $1 }

当闭包足够长、无法内联写在一行里时,尾随闭包最有用武之地。举个例子,Swift 的 Array 类型有一个 map(_:) 方法,它接受一个闭包表达式作为唯一实参。该闭包会为数组中的每个元素调用一次,并返回该元素映射后的替代值(可能是其他类型)。映射的性质和返回值的类型,由你传给 map(_:) 的闭包中的代码来决定。

把所提供的闭包应用于每个数组元素之后,map(_:) 方法返回一个新数组,其中包含所有映射后的新值,顺序与原数组中对应值的顺序相同。

下面演示如何用 map(_:) 方法配合尾随闭包,把一个 Int 值数组转换成一个 String 值数组。数组 [16, 58, 510] 被用来创建新数组 ["OneSix", "FiveEight", "FiveOneZero"]:

1
2
3
4
5
let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

上面的代码创建了一个字典,把整数数字与它们对应的英文名称映射起来,同时定义了一个整型数组,准备转换成字符串。

现在你可以用 numbers 数组创建一个 String 值数组:把一个闭包表达式作为尾随闭包传给数组的 map(_:) 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings 被推断为 [String] 类型
// 它的值是 ["OneSix", "FiveEight", "FiveOneZero"]

map(_:) 方法为数组中的每个元素调用一次这个闭包表达式。你不需要指定闭包输入参数 number 的类型,因为它可以从待映射数组中的值推断出来。

在这个例子里,变量 number 用闭包参数 number 的值初始化,以便在闭包体内修改这个值。(函数和闭包的参数始终是常量。)这个闭包表达式还指定了返回类型 String,以表明映射后的输出数组中存放的类型。

闭包表达式每次被调用时都会构造一个名为 output 的字符串。它用求余运算符(number % 10)算出 number 的最后一位数字,并用这个数字在 digitNames 字典中查找相应的字符串。这个闭包可以用来生成任何大于零的整数的字符串表示。

注意:调用 digitNames 字典的下标之后跟着一个感叹号(!), 因为字典下标返回可选值, 以表示键不存在时字典查找可能失败。 在上面的例子里,可以保证 number % 10 始终是 digitNames 字典的合法下标键, 因此用感叹号强制解包下标可选返回值中存放的 String 值。

从 digitNames 字典取出的字符串被加到 output 的前面,实际上是从后往前拼出这个数字的字符串形式。(表达式 number % 10 对 16 得到 6,对 58 得到 8,对 510 得到 0。)

接着把变量 number 除以 10。由于它是整数,除法时会向下取整,于是 16 变成 1,58 变成 5,510 变成 51。

这个过程不断重复,直到 number 等于 0,此时闭包返回 output 字符串,map(_:) 方法再把它加进输出数组。

上面的例子使用尾随闭包语法,把闭包的功能紧凑地放在它所支持的函数之后,同时不必把整个闭包包在 map(_:) 方法的外层圆括号里。

如果一个函数接受多个闭包,你就省略第一个尾随闭包的实参标签,并给其余的尾随闭包加上标签。例如,下面这个函数为相册加载一张图片:

1
2
3
4
5
6
7
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

调用这个函数加载图片时,你提供两个闭包。第一个闭包是完成处理器,在下载成功后显示图片;第二个闭包是错误处理器,向用户显示错误。

1
2
3
4
5
loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

在这个例子里,loadPicture(from:completion:onFailure:) 函数把网络任务派发到后台,并在网络任务结束时调用两个完成处理器之一。这样编写函数让你能把负责处理网络失败的代码与下载成功后更新用户界面的代码干净地分开,而不必只用一个闭包同时处理这两种情况。

注意:完成处理器可能会变得难以阅读, 尤其是当你必须嵌套多个处理器时。 另一种做法是使用异步代码, 详见并发。

捕获值

闭包可以捕获它所定义所处上下文中的常量和变量。闭包体随后可以引用并修改这些常量和变量的值,即使定义这些常量和变量的原作用域已经不存在了。

在 Swift 中,能捕获值的最简单的闭包形式,就是写在另一个函数体内部的嵌套函数。嵌套函数可以捕获外层函数的任何实参,也可以捕获外层函数中定义的任何常量和变量。

下面是一个名为 makeIncrementer 的函数示例,其中包含一个名为 incrementer 的嵌套函数。嵌套的 incrementer() 函数从周围上下文捕获了两个值:runningTotal 和 amount。捕获这些值之后,incrementer 被 makeIncrementer 作为闭包返回;每次调用它,它就按 amount 递增 runningTotal。

1
2
3
4
5
6
7
8
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer 的返回类型是 () -> Int,这意味着它返回的是一个函数而不是简单的值。返回的函数没有参数,每次调用都返回一个 Int 值。关于函数如何返回其他函数,参见函数类型作为返回类型。

makeIncrementer(forIncrement:) 函数定义了一个名为 runningTotal 的整数变量,用来存放将要返回的那个递增器的当前累计值。这个变量初始化为 0。

makeIncrementer(forIncrement:) 函数只有一个 Int 参数,实参标签是 forIncrement,参数名是 amount。传给这个参数的实参值决定了每次调用返回的递增器函数时 runningTotal 应当增加多少。makeIncrementer 函数定义了执行实际递增操作的嵌套函数 incrementer,它只是把 amount 加到 runningTotal 上并返回结果。

单独来看,嵌套的 incrementer() 函数可能显得有些奇怪:

1
2
3
4
func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer() 函数没有任何参数,却在函数体中引用了 runningTotal 和 amount。它通过在函数体中使用周围函数中 runningTotal 和 amount 的引用来做到这一点。按引用捕获保证了 makeIncrementer 调用结束后 runningTotal 和 amount 不会消失,也保证了下次调用 incrementer 函数时 runningTotal 仍然可用。

注意:作为一种优化, 如果某个值没有被闭包修改, 并且闭包创建之后该值也没有被修改, Swift 也可能改为捕获并保存该值的一份副本。

Swift 还负责变量不再需要时的所有内存管理。

下面是 makeIncrementer 的一个使用示例:

1
let incrementByTen = makeIncrementer(forIncrement: 10)

这个例子把一个名为 incrementByTen 的常量设为指向一个递增器函数,每次调用它都会给 runningTotal 变量加上 10。多次调用该函数可以看到它的行为:

1
2
3
4
5
6
incrementByTen()
// 返回 10
incrementByTen()
// 返回 20
incrementByTen()
// 返回 30

如果你创建第二个递增器,它会拥有自己保存的对一个新的、独立的 runningTotal 变量的引用:

1
2
3
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回 7

再次调用原来的递增器(incrementByTen)会继续递增它自己的 runningTotal 变量,不影响 incrementBySeven 所捕获的变量:

1
2
incrementByTen()
// 返回 40

注意:如果你把一个闭包赋给类实例的某个属性, 而这个闭包通过引用该实例或其成员捕获了该实例, 就会在闭包和实例之间造成强引用循环。 Swift 使用捕获列表来打破这类强引用循环。 更多内容参见闭包的强引用循环。

闭包是引用类型

在上面的例子里,incrementBySeven 和 incrementByTen 都是常量,但这些常量所指的闭包仍然能递增它们捕获的 runningTotal 变量。这是因为函数和闭包都是引用类型。

每当把一个函数或闭包赋给常量或变量时,你实际上是把该常量或变量设为对那个函数或闭包的引用。在上面的例子里,保持不变的是 incrementByTen 所引用的闭包的选择,而不是闭包自身的内容。

这也意味着,如果你把一个闭包赋给两个不同的常量或变量,那么这两个常量或变量引用的是同一个闭包。

1
2
3
4
5
6
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回 50

incrementByTen()
// 返回 60

上面的例子表明,调用 alsoIncrementByTen 与调用 incrementByTen 效果相同。由于两者引用同一个闭包,它们递增并返回的是同一个累计值。

逃逸闭包

当闭包作为实参传给某个函数、却在函数返回之后才被调用时,我们说这个闭包逃逸出了该函数。当你声明一个把闭包作为参数之一的函数时,可以在参数类型前写 @escaping,表示允许该闭包逃逸。

闭包逃逸的一种方式是把它存放在函数外部定义的变量中。举例来说,许多启动异步操作的函数会接收一个闭包实参作为完成处理器:函数在启动操作后就返回了,但闭包要到操作完成后才会被调用——闭包需要逃逸,以便稍后被调用。例如:

1
2
3
4
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 函数接收一个闭包作为实参,并把它添加到函数外部声明的数组中。如果你不给这个函数的参数加上 @escaping 标记,就会得到编译期错误。

如果 self 指向类的实例,那么引用 self 的逃逸闭包需要特别考虑。在逃逸闭包中捕获 self 很容易无意间造成强引用循环。关于引用循环的内容,参见自动引用计数。

通常,闭包通过在闭包体中使用变量来隐式捕获它们,但在这种情况下你需要显式捕获。如果你想捕获 self,就在使用 self 时显式写出 self,或者把 self 写进闭包的捕获列表。显式写出 self 让你能够表达自己的意图,也提醒你确认没有引用循环。例如在下面的代码中,传给 someFunctionWithEscapingClosure(_:) 的闭包显式引用了 self;相比之下,传给 someFunctionWithNonescapingClosure(_:) 的闭包是非逃逸闭包,因此它可以隐式引用 self。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 输出 "200"。

completionHandlers.first?()
print(instance.x)
// 输出 "100"。

下面这个版本的 doSomething() 通过把 self 写进闭包的捕获列表来捕获它,然后隐式引用 self:

1
2
3
4
5
6
7
class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

如果 self 是结构体或枚举的实例,你总是可以隐式引用 self。不过当 self 是结构体或枚举的实例时,逃逸闭包不能捕获对 self 的可变引用。结构体和枚举不允许共享可变性,详见结构体与枚举是值类型。

1
2
3
4
5
6
7
struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // OK
        someFunctionWithEscapingClosure { x = 100 }     // 错误
    }
}

上面例子里对 someFunctionWithEscapingClosure 函数的调用会报错,因为它位于一个 mutating 方法中,因此 self 是可变的。这违反了"逃逸闭包不能为结构体捕获 self 的可变引用"这一规则。

自动闭包

自动闭包是一种自动创建的闭包,用来包装作为实参传给函数的表达式。它不接受任何实参,调用时会返回它所包装表达式的值。这一语法上的便利让你可以用普通表达式代替显式闭包,从而省略函数参数外面的花括号。

调用接收自动闭包的函数很常见,但实现这类函数并不常见。例如,assert(condition:message:file:line:) 函数的 condition 和 message 参数都接收自动闭包:它的 condition 参数只在调试构建中求值,而 message 参数只在 condition 为 false 时才求值。

自动闭包让你能够延迟求值,因为其中的代码要等到你调用这个闭包时才会运行。对于有副作用或计算代价高昂的代码,延迟求值很有用,因为它让你能控制这些代码何时求值。下面的代码展示了闭包如何延迟求值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 输出 "5"。

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 输出 "5"。

print("Now serving \(customerProvider())!")
// 输出 "Now serving Chris!"。
print(customersInLine.count)
// 输出 "4"。

尽管闭包中的代码会移除 customersInLine 数组的第一个元素,但直到闭包真正被调用时,数组元素才会被移除。如果闭包始终未被调用,闭包中的表达式就永远不会求值,也就是说数组元素永远不会被移除。注意 customerProvider 的类型不是 String,而是 () -> String——一个没有参数、返回字符串的函数。

把闭包作为实参传给函数时,你也会得到同样的延迟求值行为。

1
2
3
4
5
6
// customersInLine 是 ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 输出 "Now serving Alex!"。

上面清单中的 serve(customer:) 函数接受一个返回顾客名字的显式闭包。下面的 serve(customer:) 版本做同样的事,但不用显式闭包,而是通过给参数类型加上 @autoclosure 特性来接受自动闭包。现在你可以像函数接受的是 String 实参而不是闭包那样调用它:实参会自动转换成闭包,因为 customerProvider 参数的类型带有 @autoclosure 特性标记。

1
2
3
4
5
6
// customersInLine 是 ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 输出 "Now serving Ewa!"。

注意:过度使用自动闭包会让代码难以理解。 上下文和函数名应当清楚地表明 求值被推迟了。

如果你需要允许逃逸的自动闭包,就同时使用 @autoclosure 和 @escaping 两个特性。@escaping 特性在上文的逃逸闭包中已经介绍过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// customersInLine 是 ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 输出 "Collected 2 closures."。
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 输出 "Now serving Barry!"。
// 输出 "Now serving Daniella!"。

在上面的代码中,collectCustomerProviders(_:) 函数并没有调用作为 customerProvider 实参传给它的闭包,而是把这个闭包追加到 customerProviders 数组中。该数组声明在函数作用域之外,这意味着数组中的闭包可以在函数返回之后执行。因此,customerProvider 实参的值必须被允许逃逸出函数作用域。