Swift - Closure

闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数比较相似。

闭包表达式

闭包表达式会创建一个闭包,在其他语言中也叫 lambda 或匿名函数。跟函数一样,闭包包含了待执行的代码,不同的是闭包还会捕获所在环境中的常量和变量。它的形式如下

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

闭包还有几种特殊的形式,能让闭包使用起来更加简洁:

  • 闭包可以省略它的参数和返回值的类型。如果省略了参数名和所有的类型,也要省略 in 关键字。如果被省略的类型无法被编译器推断,那么就会导致编译错误。
  • 闭包可以省略参数名,参数会被隐式命名为 $ 加上其索引位置,例如 $0$1$2 分别表示第一个、第二个、第三个参数,以此类推。
  • 如果闭包中只包含一个表达式,那么该表达式的结果就会被视为闭包的返回值。表达式结果的类型也会被推断为闭包的返回类型。

尾随闭包

如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函数体部分
}

// 以下是不使用尾随闭包进行函数调用
someFunctionThatTakesAClosure(closure: {
// 闭包主体部分
})

// 以下是使用尾随闭包进行函数调用
someFunctionThatTakesAClosure() {
// 闭包主体部分
}

自动闭包

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

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

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())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"

@autoclosure

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

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

逃逸闭包与非逃逸闭包

从 Swift 3.0 开始,非逃逸闭包变成了闭包参数的默认形式。如果你想允许一个闭包参数逃逸,需要给这个类型增加一个 @escaping 的标注。

为什么区分逃逸与非逃逸

闭包会强引用它捕获的所有对象,如果在闭包中访问当前对象中的属性或方法,会持有当前对象,很容易导致循环引用。

使用非逃逸的闭包不会产生循环引用,编译器可以保证在函数返回时闭包会释放它捕获的所有对象。因此,编译器只要求在逃逸闭包中明确对 self 的强引用。

使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化。例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用。此外,如果闭包是一个非逃逸闭包,它的上下文的内存可以保存在栈上而不是堆上。

逃逸闭包

一个接受逃逸闭包作为参数的函数,逃逸闭包(可能)会在函数返回之后才被调用————也就是说,闭包逃离了函数的作用域。

应用场景

逃逸闭包通常与异步控制流相关联,如下例所示:

  • 一个函数开启了一个后台任务后立即返回,然后通过一个完成回调(completion handler)报告后台任务的结果。
  • 一个视图类把『按钮点击事件执行的操作』封装成一个闭包,并存储为自身的属性。每次用户点击按钮时,都会调用该闭包。闭包会逃离属性的设置器(setter)。
  • 使用 DispatchQueue.async 在派发队列(dispatch queue)上安排了一个异步执行的任务。这个闭包任务的生命周期会比 async 的作用域活得更长久。

函数类型的变量总是逃逸的

即使没有明确的标注,指向/保存函数类型(闭包)的变量或属性,都是自动逃逸的(实际上,如果你显式添加一个 @escaping 也会报错)。

这其实很合理,因为赋值给一个变量隐性地允许该值逃逸到变量的作用域中,而非逃逸闭包不允许这种行为。这可能会让人困惑,但一个未做任何标注的闭包在参数列表中与其他任何情况都不同。

可选型的闭包总是逃逸的

即便闭包被用作参数,但是当闭包被包裹在其他类型(例如元组、枚举的 case 以及可选型)中的时候,闭包仍旧是逃逸的。由于在这种情况下闭包不再是即时的参数,它会自动变成逃逸闭包。

1
2
3
4
5
6
/// Applies `f` to `n` and returns the result.
/// Returns `n` unchanged if `f` is nil.
func transform(_ n: Int, with f: ((Int) -> Int)?) -> Int {
guard let f = f else { return n }
return f(n)
}

这里函数 f 是逃逸的,因为 ((Int) -> Int)?Optional<(Int) -> Int> 的缩写,即函数类型不在一个即时参数位上。

类型别名总是逃逸的

在 Swift 3.0 中,你不能向 typealiases 中添加逃逸或者非逃逸的标注。如果你在函数声明中对一个函数类型的参数使用了类型别名(typealias),这个参数总会被视为逃逸的。

非逃逸闭包

闭包默认为非逃逸,并不是说没有 @escaping 标注就是非逃逸闭包。

非逃逸的闭包有一个默认规则:它只能应用到即时函数的参数列表位,也就是说任何作为参数传入的闭包。所有其他类型的闭包都是逃逸的。

即时函数:也就是在传入后立即执行,如果在函数返回之后才被调用,则不是即时函数,且此时为逃逸闭包。

即时的参数位 immediate parameter position

最简单的情况就像 map:这个函数接受一个立即执行的闭包参数。

1
func map<T>(_ transform: (Iterator.Element) -> T) -> [T]

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

闭包会使用强引用指向它们,可以通过捕获列表来显式指定它的捕获行为。

捕获列表

捕获列表在参数列表之前,由中括号括起来,里面是由逗号分隔的一系列表达式。一旦使用了捕获列表,就必须使用 in 关键字,即使省略了参数名、参数类型和返回类型。

1
2
3
closure { print(self.title) }                         // 以强引用捕获
closure { [weak self] in print(self?.title ?? "") } // 以弱引用捕获
closure { [unowned self] in print(self.title) } // 以无主引用捕获

捕获列表中的项会在闭包创建时被初始化。每一项都会用闭包附近作用域中的同名常量或者变量的值初始化。例如下面的代码示例中,捕获列表包含 a 而不包含 b,这将导致这两个变量具有不同的行为。

1
2
3
4
5
6
7
8
9
10
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}

a = 10
b = 10
closure()
// 打印 "0 10"

弱引用与无主引用

在闭包和捕获的实例互相引用,并且闭包的销毁在捕获的实例前面时,应该将闭包内的捕获定义为无主引用。捕获的实例如果变成 nil 后,再调用无主引用就会发生崩溃。
相反的,在被捕获的引用可能会变为nil时,将闭包内的捕获定义为弱引用。弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在。

参考

闭包
闭包表达式
可选型的非逃逸闭包
解决闭包引起的循环强引用