可选型的非逃逸闭包
Swift 的闭包分为 逃逸 与 非逃逸 两种。一个接受逃逸闭包作为参数的函数,逃逸闭包(可能)会在函数返回之后才被调用————也就是说,闭包逃离了函数的作用域。 逃逸闭包通常与异步控制流相关联,如下例所示:
与之对应的 DispatchQueue.sync,它会一直等到任务闭包执行完毕后才返回——闭包永远不会逃逸。map 以及标准库中其他的序列和数组的算法也是非逃逸的。 为什么区分闭包的逃逸性与非逃逸性如此重要?简单来说,是为了管理内存。一个闭包会强引用它捕获的所有对象————如果你在闭包中访问了当前对象中的任意属性或实例方法,闭包会持有当前对象,因为这些方法和属性都隐性地携带了一个 这种方式很容易导致循环引用,这解释了为什么编译器会要求你在闭包中显式地写出对 然而,使用非逃逸的闭包不会产生循环引用————编译器可以保证在函数返回时闭包会释放它捕获的所有对象。因此,编译器只要求在逃逸闭包中明确对 使用非逃逸闭包的另一个好处是编译器可以应用更多强有力的性能优化。例如,当明确了一个闭包的生命周期的话,就可以省去一些保留(retain)和释放(release)的调用。此外,如果闭包是一个非逃逸闭包,它的上下文的内存可以保存在栈上而不是堆上————虽然我不确定当前的编译器是否执行了这个优化(一篇公布于 2016 年 3 月的错误报告显示当时并没有执行)。 闭包默认是非逃逸的...从 Swift 3.0 开始,非逃逸闭包变成了闭包参数的默认形式。如果你想允许一个闭包参数逃逸,需要给这个类型增加一个 class DispatchQueue { ... func async(/* other params omitted */,execute work: @escaping () -> Void) func sync<T>(execute work: () throws -> T) rethrows -> T } 在 Swift 3 之前,完全是另外一回事:逃逸是默认状态,你可以添加 ...但是只能作为即时函数的参数关于非逃逸的闭包有一个默认规则:它只能应用到即时函数的参数列表位,也就是说任何作为参数传入的闭包。所有其他类型的闭包都是逃逸的。 即时的参数位是什么意思?让我们看一些示例。最简单的情况就像 map:这个函数接受一个立即执行的闭包参数。正如我们所看到的,这个闭包是一个非逃逸的(我从 map 的真实签名中省略了一些无关、不重要的细节): func map<T>(_ transform: (Iterator.Element) -> T) -> [T] 函数类型的变量总是逃逸的与此相比。即使没有明确的标注,指向/保存函数类型(闭包)的变量或属性,都是自动逃逸的(实际上,如果你显式添加一个 @escaping 也会报错)。这其实很合理,因为赋值给一个变量隐性地允许该值逃逸到变量的作用域中,而非逃逸闭包不允许这种行为。这可能会让人困惑,但一个未做任何标注的闭包在参数列表中与其他任何情况都不同。 可选型的闭包总是逃逸的更令人惊讶的是,即便闭包被用作参数,但是当闭包被包裹在其他类型(例如元组、枚举的 case 以及可选型)中的时候,闭包仍旧是逃逸的。由于在这种情况下闭包不再是即时的参数,它会自动变成逃逸闭包。因此,在 Swift 3.0 中,当你编写一个接受函数类型参数的函数时,该参数不能同时是可选型和非逃逸的。思考下面这个精心设计的例子:函数 /// 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 是逃逸的,因为 将可选参数替换为默认实现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) } 我添加了一些打印语句来演示哪个函数被调用。用不同的参数来测试一下。不出意外,当你传入 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 现在使用字面量的闭包表达式来调用高阶函数的方式已经习以为常,所以在大多数情况下你都可以选用这个令人愉悦的方式(即非逃逸,不需要担心循环引用),同时仍然可以选择传入 类型别名总是逃逸的最后要注意的是,在 Swift 3.0 中,你不能向
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |