7. Swift 闭包 Closure.png

闭包

闭包是特殊的函数,当函数作为参数或返回值,或者是匿名函数时,可称之为闭包。

闭包可以根据上下文推断参数和返回值的数据类型,所以有时可以省略不指定参数和返回值的数据类型。

函数和闭包都是引用类型。无论将函数或闭包赋值给一个常量还是变量,实际上都是将常量或变量的值设置为对应函数或闭包的引用,指向同一个闭包。

方法定义

  1. func name(parameters) -> return type {
  2. function body
  3. }

闭包定义

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

例子

简单例子

  1. let test = { (a: String, b: String) -> String in
  2. a + b
  3. }
  4. print(test("Hello, ", "Swift")) // Hello, Swift

最简闭包

单行表达式 {} 并且隐式返回值(也就是闭包体只有一行代码,可以省略return)

  1. let test = { 10 }
  2. print(test()) // 10

简化参数

如 $0, $1…(从 0 开始,表示第 i 个参数)

  1. let test = { print($0, $1, $2) }
  2. test(1, 2, 3) // 1 2 3

尾随闭包

如果闭包是函数的最后一个参数,则称之为尾随闭包,可以将其代码简化

  1. func calculate(a: Int, b: Int, fn: (Int, Int) -> Int) -> Int {
  2. return fn(a, b)
  3. }
  4. let (a, b) = (1, 2)
  5. let r1 = calculate(a: a, b: b) { $0 + $1 }
  6. let r2 = calculate(a: a, b: b) { $0 - $1 }
  7. print(r1, r2) // 3 -1

综合例子

  1. let names = ["AT", "AE", "D", "S", "BE"]
  2. func compare(a: String, b: String) -> Bool {
  3. return a < b
  4. }
  5. print(names.sorted(by: compare)) // 方法
  6. print(names.sorted(by: { $0 < $1})) // 闭包
  7. print(names.sorted() { $0 < $1 }) // 尾随闭包
  8. print(names.sorted { $0 < $1 }) // 尾随闭包,无参时可省略()

捕获变量

闭包之所以称之为闭包,是因为其可以捕获一定作用域内的常量或者变量进而闭合并包裹着

下面代码的闭包 fn 捕获了当前函数作用域的 total, step 变量(参数、内部变量、内存常量都可捕获)

  1. // func generator(step: Int) -> () -> Int {
  2. func generator() -> () -> Int {
  3. var total = 0, step = 1
  4. func fn() -> Int {
  5. total += step
  6. return total
  7. }
  8. return fn
  9. }
  10. let next = generator()
  11. print( next() ) // 1
  12. print( next() ) // 2
  13. let next2 = generator()
  14. print( next2() ) // 1
  15. print( next2() ) // 2
  16. let next3 = next2
  17. print( next3() ) // 3
  18. print( next2() ) // 4

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。 这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。 这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

一般闭包(花括号 {} 包裹的语句块):

  1. func make(_ closure: () -> String ) {
  2. print("do \(closure()) !")
  3. }
  4. var array = ["a", "b", "c", "d"]
  5. make{ array.remove(at: 0) }
  6. make({ array.remove(at: 0) })
  7. make {
  8. let a = 10
  9. return "\(a * 10)"
  10. }
  11. // do a !
  12. // do b !
  13. // do 100 !

自动闭包:与上面相比,明显的可以看到:它的闭包表达式中只有一条语句,且没有被花括号 {} 包裹住

  1. func make_autoclosure(_ closure: @autoclosure () -> String ) {
  2. print("do_autoclosure \(closure()) !")
  3. }
  4. var array = ["a", "b", "c", "d"]
  5. make_autoclosure( array.remove(at: 0) ) // a
  1. func myassert(_ closure: @autoclosure () -> Bool, _ trueMessage: String?) {
  2. if closure() {
  3. if trueMessage != nil {
  4. print(trueMessage!)
  5. }
  6. }
  7. }
  8. myassert(1 < 2, "条件为真")

逃逸闭包 与 非逃逸闭包

参考:
https://www.jianshu.com/p/d386392fa8c0
https://blog.csdn.net/freedom12354/article/details/82469119

一个接受闭包作为参数的函数,该闭包可能在函数返回后才被调用,也就是说这个闭包逃离了函数的作用域,这种闭包称为逃逸闭包。当你声明一个接受闭包作为形式参数的函数时,你可以在形式参数前写 @escaping 来明确闭包是允许逃逸,并且在闭包中显式地引用 self .

逃逸闭包的生命周期

  1. 闭包作为参数传递给函数
  2. 退出函数
  3. 闭包被调用,闭包生命周期结束

逃逸闭包的生命周期长于函数,函数退出的时候,逃逸闭包的引用仍被其他对象持有,不会在函数结束时释放。

逃逸闭包的使用场景

  1. 异步调用:

如果需要调度队列中异步调用闭包,这个队列会持有闭包的引用,至于什么时候调用闭包,或者闭包什么时候运行结束都是不可预知的。
比如 loadData 方法进行网络请求,当网络响应后调用闭包。可是 loadData 这个方法早已就执行完毕,这样闭包就脱离了 loadData 方法的作用域,并还持有相关对象的引用。

逃逸闭包的测试案例

  1. typealias ResponseBlock = (_ data: Dictionary<String, Any>?, _ error: Error?) -> Void
  2. func getRequest(url: String, body: Dictionary<String, Any>?,
  3. _ handle: @escaping ResponseBlock) {
  4. handle(Dictionary(), nil)
  5. }
  6. class Test {
  7. var url = "http://www.baidu.com/"
  8. func testEscaping() {
  9. getRequest(url: url, body: nil) { (data, error) in
  10. print(self.url) // 此处必须显式地引用 self,否则编译错误
  11. }
  12. }
  13. }
  14. let tt = Test()
  15. tt.testEscaping()

非逃逸闭包

一个接受一个闭包作为参数的函数,闭包会在函数结束前被调用,即相关引用已被释放。

Swift4 好像把 @noescape 移除了,不过默认闭包都是隐式的非逃逸闭包

  1. typealias EmptyBlock = () -> Void
  2. func noescape(_ closure: EmptyBlock) {
  3. closure()
  4. }
  5. class Test {
  6. var url = "http://www.baidu.com/"
  7. func testNoescape() {
  8. noescape {
  9. print(url)
  10. }
  11. }
  12. }
  13. let tt = Test()
  14. tt.testNoescape()

区分逃逸闭包和非逃逸闭包

使用逃逸闭包能规避循环引用的潜在风险

为了管理内存,闭包会强引用它捕获的所有对象,比如你在闭包中访问了当前控制器的属性、函数,编译器会要求你在闭包中显示 self 的引用,这样闭包会持有当前对象,容易导致循环引用。

非逃逸闭包不会产生循环引用,它会在函数作用域内释放,编译器可以保证在函数结束时闭包会释放它捕获的所有对象;使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化,例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用;此外非逃逸闭包它的上下文的内存可以保存在栈上而不是堆上。

综上所述,如果没有特别需要,开发中使用非逃逸闭包是有利于内存优化的,所以苹果把闭包区分为两种,特殊情况时再使用逃逸闭包。

  1. var handlers: [() -> Void] = []
  2. func test(handler: @escaping () -> Void) {
  3. handlers.append(handler)
  4. }

比如,在上面的例子中,编译器会自动提示添加 @escaping ,这是因为闭包作为参数并没有在方法结束前调用,而是存储在数组中了,闭包调用及结束时机是不可预知的,所以这里应该使用逃逸闭包。