Swift 闭包

Swift 中的闭包是自包含的函数代码块,可以在代码中被传递和使用。类似于OC中的Block以及其他函数的匿名函数。闭包可以捕获和存储其所在上下文中任意常量和变量的引用,被称为包裹常量和变量。Swift可以为你管理在捕获过程中涉及到的所有内存操作

函数与闭包的关系

全局和嵌套函数实际上也是特殊的閉包,闭包有如下三种形式:
1.全局函数是一個有名字但不会捕获任何值的闭包
2.嵌套函数是一個有名字並可以捕获其封闭函数域内值的闭包
3.闭包表达式是一個利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

  • 闭包表达式
  • 尾随闭包
  • 值捕获
  • 闭包是引用类型
  • 逃逸闭包
  • 自动闭包

闭包表达式一般形式:

1
2
3
{ (parameters) -> returnType in
statements
}

下面以 Swift 标准库中的 sorted(by:) 方法为例,展示闭包的各种书写格式。它会根据闭包函数中的实现将数组中的值进行排序,返回一个与原数组大小相同,包含相同元素且重新排序的新数组。原数组不会被 sorted(by:) 方法修改

最复杂的形式

原数组: let names = ["Linda", "Cindy", "Benty", "Dandy"]
排序后数组:sortNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 < s2 } ) print(sortNames) // [ "Benty", "Cindy", "Dandy","Linda"]
如果第一个字符串s1小于 第二个字符串s2, 函数会返回 true,在新的数组中 s1 应该出现在 s2 前。对于字符串中的字符来说,“小于”表示“按照字母顺序较晚出现”。这意味着字母 “A” 小于字母 “B” ,字符串 “Ab” 小于字符串 “Ac”。该闭包将进行字母顺序排序,”Benty” 将会排在 “Cindy” 之前。

上下文推断类型

排序闭包函数作为 sorted(by:) 方法的参数传入,Swift 可以推断其参数和返回值的类型。sorted(by:) 方法被一个字符串数组调用,因此其参数必须是 (String, String) -> Bool 类型。这意味着 (String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
sortNames = names.sorted(by: { s1, s2 in return s1 < s2 } )
尽管如此,你仍然可以明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包。而在 sorted(by:) 方法这个例子里,显然闭包的目的就是排序。由于这个闭包是为了处理字符串数组的排序,因此读者能够推测出这个闭包是用于字符串处理的。

单表达式闭包隐式返回

sortNames = names.sorted(by: { s1, s2 in s1 < s2 } )
在这个例子中,sorted(by:) 方法的参数类型明确了闭包必须返回一个 Bool 类型值。因为闭包函数体只包含了一个单一表达式(s1 < s2),该表达式返回 Bool 类型值,因此这里没有歧义,return 关键字可以省略

参数名称缩写

Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 来顺序调用闭包的参数,以此类推。如果你在闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,并且对应参数名称缩写的类型会通过函数类型进行推断。in关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
sortNames = names.sorted(by: { $0 < $1 } )
在这个例子中,$0和$1表示闭包中第一个和第二个 String 类型的参数。

运算符方法

实际上还有一种更简短的方式来编写上面例子中的闭包表达式。Swift 的 String 类型定义了关于小于号(<)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个小于号,Swift 可以自动推断出你想使用小于号的字符串函数实现:
sortNames = names.sorted(by: <)

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签。
sortNames = names.sorted() { $0 < $1 }
如果闭包表达式是函数或方法的唯一参数,则当你使用尾随闭包时,你甚至可以把 () 省略掉:sortNames =names.sorted { $0 < $1 }

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。
举个例子,这有一个叫做 makeIncrementor 的函数,其包含了一个叫做 incrementor 的嵌套函数。嵌套函数 incrementor() 从上下文中捕获了两个值,runningTotalamount。捕获这些值之后,makeIncrementorincrementor 作为闭包返回。每次调用 incrementor 时,其会以 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 incremented
}

makeIncrementor 返回类型为 () -> Int。这意味着其返回的是一个函数,而非一个简单类型的值。该函数在每次调用时不接受参数,只返回一个 Int 类型的值 makeIncrementer(forIncrement:) 函数定义了一个初始值为 0 的整型变量 runningTotal,用来存储当前总计数值。该值为 incrementor 的返回值
makeIncrementer(forIncrement:) 有一个 Int 类型的参数,其外部参数名为 forIncrement,内部参数名为 amount,该参数表示每次
incrementor 被调用时 runningTotal 将要增加的量。makeIncrementer 函数还定义了一个嵌套函数 incrementor,用来执行实际的增加操作。该函数简单地使 runningTotal 增加 amount,并将其返回。
如果我们单独考虑嵌套函数 incrementer(),会发现它有些不同寻常:

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

incrementer() 函数并没有任何参数,但是在函数体内访问了 runningTotalamount 变量。这是因为它从外围函数捕获了
runningTotalamount 变量的引用。捕获引用保证了 runningTotalamount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在。

闭包是引用类型

上面的例子中,incrementBySeven 和 incrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型。无论你将函数或闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用。上面的例子中,指向闭包的引用 incrementByTen 是一个常量,而并非闭包内容本身。这也意味着如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // 返回的值为50

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:

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

someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping 就会得到一个编译错误。将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 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
21
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
var x = 10
func doSomething() {
someFunctionWithEscapingClosure {
//逃逸闭包 需要显式地引用 self
//调用doSomething 不会执行闭包
self.x = 100
}
someFunctionWithNonescapingClosure {
//因为不是逃逸闭包,调用doSomething 执行闭包
x = 200
}
}
doSomething()
print(x) // 打印出 "200"
completionHandlers.first?()
print(x) // 打印出 "100" 逃逸闭包调用

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。
我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当
condition 参数为 false 时被计算求值。
自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

1
2
3
4
5
6
7
8
9
10
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())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"

尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider 的类型不是
String ,而是() -> String ,一个没有参数且返回值为 String 的函数。将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

1
2
3
4
5
6
let 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
let customersInLine = ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印 "Now serving Ewa!"

注意 过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure@escaping 属性。@escaping 属性的讲解见上面的逃逸闭包。

1
2
3
4
5
6
7
8
9
10
11
12
customersInLine i= ["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 参数必须允许“逃逸”出函数作用域。

结束

本文内容摘自中文版 Apple 官方 Swift 教程,对于初学swift的朋友有较大帮助,下面是苹果官方文档链接和github中文文档链接 官方文档 中文文档

Contents
  1. 1. 函数与闭包的关系
  2. 2. 闭包表达式一般形式:
  3. 3. 最复杂的形式
  4. 4. 上下文推断类型
  5. 5. 单表达式闭包隐式返回
  6. 6. 参数名称缩写
  7. 7. 运算符方法
  8. 8. 尾随闭包
  9. 9. 值捕获
  10. 10. 闭包是引用类型
  11. 11. 逃逸闭包
  12. 12. 自动闭包
  13. 13. 结束
|