可选型的非逃逸闭包

做者:Ole Begemann,原文连接,原文日期:2016/10/10
译者:Cwift;校对:walkingway;定稿:CMBhtml

Swift 的闭包分为 逃逸非逃逸 两种。一个接受逃逸闭包做为参数的函数,逃逸闭包(可能)会在函数返回以后才被调用————也就是说,闭包逃离了函数的做用域。git

逃逸闭包一般与异步控制流相关联,以下例所示:github

  • 一个函数开启了一个后台任务后当即返回,而后经过一个完成回调(completion handler)报告后台任务的结果。算法

  • 一个视图类把『按钮点击事件执行的操做』封装成一个闭包,并存储为自身的属性。每次用户点击按钮时,都会调用该闭包。闭包会逃离属性的设置器(setter)。swift

  • 你使用 [DispatchQueue.async]() 在派发队列(dispatch queue)上安排了一个异步执行的任务。这个闭包任务的生命周期会比 async 的做用域活得更长久。api

与之对应的 DispatchQueue.sync,它会一直等到任务闭包执行完毕后才返回——闭包永远不会逃逸。map 以及标准库中其余的序列和数组的算法也是非逃逸的。数组

为何区分闭包的逃逸性与非逃逸性如此重要?

简单来讲,是为了管理内存。一个闭包会强引用它捕获的全部对象————若是你在闭包中访问了当前对象中的任意属性或实例方法,闭包会持有当前对象,由于这些方法和属性都隐性地携带了一个 self 参数。安全

这种方式很容易致使循环引用,这解释了为何编译器会要求你在闭包中显式地写出对 self 的引用。这迫使你考虑潜在的循环引用,并使用捕获列表手动处理。性能优化

然而,使用非逃逸的闭包不会产生循环引用————编译器能够保证在函数返回时闭包会释放它捕获的全部对象。所以,编译器只要求在逃逸闭包中明确对 self 的强引用。显然,使用非逃逸闭包是一个更加愉悦的方案。闭包

使用非逃逸闭包的另外一个好处是编译器能够应用更多强有力的性能优化。例如,当明确了一个闭包的生命周期的话,就能够省去一些保留(retain)和释放(release)的调用。此外,若是闭包是一个非逃逸闭包,它的上下文的内存能够保存在栈上而不是堆上————虽然我不肯定当前的编译器是否执行了这个优化(一篇公布于 2016 年 3 月的错误报告显示当时并无执行)。

闭包默认是非逃逸的...

从 Swift 3.0 开始,非逃逸闭包变成了闭包参数的默认形式。若是你想容许一个闭包参数逃逸,须要给这个类型增长一个 @escaping 的标注。例如, DispatchQueue.async (逃逸)和 DispatchQueue.sync(非逃逸)的定义:

class DispatchQueue {
    ...
    func async(/* other params omitted */, execute work: @escaping () -> Void)
    func sync<T>(execute work: () throws -> T) rethrows -> T
}

在 Swift 3 以前,彻底是另一回事:逃逸是默认状态,你能够添加 @noescape 来覆盖此状态。新的行为更好,由于在默认状态下是安全的:遇到有潜在循环引用的状况时,一个方法调用必须显式地予以标注。所以,@escaping 标识符还有警示开发者的做用。

...可是只能做为即时函数的参数

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

即时的参数位是什么意思?

让咱们看一些示例。最简单的状况就像 map:这个函数接受一个当即执行的闭包参数。正如咱们所看到的,这个闭包是一个非逃逸的(我从 map 的真实签名中省略了一些无关、不重要的细节):

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

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

与此相比。即便没有明确的标注,指向/保存函数类型(闭包)的变量或属性,都是自动逃逸的(实际上,若是你显式添加一个 @escaping 也会报错)。这其实很合理,由于赋值给一个变量隐性地容许该值逃逸到变量的做用域中,而非逃逸闭包不容许这种行为。这可能会让人困惑,但一个未作任何标注的闭包在参数列表中与其余任何状况都不一样。

可选型的闭包老是逃逸的

更使人惊讶的是,即使闭包被用做参数,可是当闭包被包裹在其余类型(例如元组、枚举的 case 以及可选型)中的时候,闭包仍旧是逃逸的。因为在这种状况下闭包再也不是即时的参数,它会自动变成逃逸闭包。所以,在 Swift 3.0 中,当你编写一个接受函数类型参数的函数时,该参数不能同时是可选型和非逃逸的。思考下面这个精心设计的例子:函数 transform 接受一个整数 n 以及一个可选型的变换函数 f。正常状况下它返回 f(n),而 f 为空值时返回 n。

/// 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 团队已经意识到了这个问题,而且会在未来的版本中解决它。在那以前,对这个问题有必定了解是很是重要的。目前没有办法让一个可选型的闭包变成非逃逸的,可是在许多状况下,你能够经过为闭包提供一个默认值的方式来避免使用可选型参数。在咱们的例子中,默认值是一个特定的函数,返回一个不可变的参数:

/// Uses a default implementation for `f` if omitted
func transform(_ n: Int, with f: (Int) -> Int = { $0 }) -> Int {
    return f(n)
}

使用重载提供一个可选型和一个非逃逸的变体

若是不能提供默认值,Michael Ilseman 建议使用重载解决————你能够编写两个版本的方法,一个带有可选型(逃逸)函数参数,另外一个带有非可选型的非逃逸参数:

// Overload 1: optional, escaping
func transform(_ n: Int, with f: ((Int) -> Int)?) -> Int {
    print("Using optional overload")
    guard let f = f else { return n }
    return f(n)
}

// Overload 2: non-optional, non-escaping
func transform(_ input: Int, with f: (Int) -> Int) -> Int {
    print("Using non-optional overload")
    return f(input)
}

我添加了一些打印语句来演示哪一个函数被调用。用不一样的参数来测试一下。不出意外,当你传入 nil,类型检查器选择第一个重载的版本,由于只有它兼容输入的参数类型:

swfit
transform(10, with: nil) // → 10
// Using optional overload

若是你传递一个可选函数类型的闭包,一样如此:

let f: ((Int) -> Int)? = { $0 * 2 }
transform(10, with: f) // → 20
// Using optional overload

即使变量的值不是可选型的,Swift 依旧选择第一个版本的重载。这是由于存储在变量中的函数是自动逃逸的,所以与指望传入非逃逸参数的第二个重载版本不兼容:

let g: (Int) -> Int = { $0 * 2 }
transform(10, with: g) // → 20
// Using optional overload

可是,当你传递一个闭包的表达式,即函数字面量到相应的位置时,状况会变得不同。此时会选择第二个非逃逸的版本:

transform(10) { $0 * 2 } // → 20
// Using non-optional overload

如今使用字面量的闭包表达式来调用高阶函数的方式已经习觉得常,因此在大多数状况下你均可以选用这个使人愉悦的方式(即非逃逸,不须要担忧循环引用),同时仍然能够选择传入 nil。若是你决定这么作,必定要在文档中明确标注你须要两个重载的理由。

类型别名老是逃逸的

最后要注意的是,在 Swift 3.0 中,你不能向 typealiases 中添加逃逸或者非逃逸的标注。若是你在函数声明中对一个函数类型的参数使用了类型别名(typealias),这个参数总会被视为逃逸的。这个 bug 已经在主分支上修复了,应该会出如今下一个 release 版本中。

本文由 SwiftGG 翻译组翻译,已经得到做者翻译受权,最新文章请访问 http://swift.gg

相关文章
相关标签/搜索