【问题标题】:Closure use of non-escaping parameter may allow it to escape非转义参数的闭包使用可能允许它转义
【发布时间】:2016-12-23 18:31:46
【问题描述】:

我有一个协议:

enum DataFetchResult {
    case success(data: Data)
    case failure
}

protocol DataServiceType {
    func fetchData(location: String, completion: (DataFetchResult) -> (Void))
    func cachedData(location: String) -> Data?
}

通过示例实现:

    /// An implementation of DataServiceType protocol returning predefined results using arbitrary queue for asynchronyous mechanisms.
    /// Dedicated to be used in various tests (Unit Tests).
    class DataMockService: DataServiceType {

        var result      : DataFetchResult
        var async       : Bool = true
        var queue       : DispatchQueue = DispatchQueue.global(qos: .background)
        var cachedData  : Data? = nil

        init(result : DataFetchResult) {
            self.result = result
        }

        func cachedData(location: String) -> Data? {
            switch self.result {
            case .success(let data):
                return data
            default:
                return nil
            }
        }

        func fetchData(location: String, completion: (DataFetchResult) -> (Void)) {

            // Returning result on arbitrary queue should be tested,
            // so we can check if client can work with any (even worse) implementation:

            if async == true {
                queue.async { [weak self ] in
                    guard let weakSelf = self else { return }

                    // This line produces compiler error: 
                    // "Closure use of non-escaping parameter 'completion' may allow it to escape"
                    completion(weakSelf.result)
                }
            } else {
               completion(self.result)
            }
        }
    }

上面的代码在 Swift3 (Xcode8-beta5) 中编译并运行,但不再适用于 beta 6。你能指出根本原因吗?

【问题讨论】:

  • 这是一个非常 great article 关于为什么在 Swift 3 中这样做的原因
  • 我们必须这样做是没有意义的。没有其他语言需要它。

标签: swift xcode swift3 closures


【解决方案1】:

这是由于函数类型参数的默认行为发生了变化。在 Swift 3 之前(特别是 Xcode 8 beta 6 附带的构建),它们将默认为转义 - 你必须标记它们 @noescape 以防止它们被存储或捕获,这保证它们不会超过函数调用的持续时间。

但是,现在@noescape 是函数类型参数的默认值。如果你想存储或捕获这样的函数,你现在需要标记它们@escaping

protocol DataServiceType {
  func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)
  func cachedData(location: String) -> Data?
}

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void) {
  // ...
}

有关此更改的更多信息,请参阅Swift Evolution proposal

【讨论】:

  • 但是,你如何使用闭包以防止其逃逸?
  • @EnekoAlonso 不完全确定您在问什么——您可以直接在函数本身中调用非转义函数参数,也可以在非转义闭包中捕获时调用它。在这种情况下,由于我们正在处理异步代码,因此无法保证async 函数参数(以及因此completion 函数)将在fetchData 退出之前被调用——因此必须是@escaping
  • 感觉很难看,我们必须将@escaping 指定为协议的方法签名......这是我们应该做的吗?建议不说! :S
  • @Sajjon 目前,您确实需要将协议要求中的@escaping 参数与该要求的实现中的@escaping 参数匹配(反之亦然,用于非转义参数)。在 Swift 2 中 @noescape 也是如此。
【解决方案2】:

由于@noescape 是默认设置,因此有两个选项可以修复错误:

1) 正如@Hamish 在他的回答中指出的那样,如果您确实关心结果并且真的希望它逃脱,只需将完成标记为@escaping(@Lukasz 的问题中可能就是这种情况,以单元测试为例和异步完成的可能性)

func fetchData(location: String, completion: @escaping (DataFetchResult) -> Void)

2) 在您不关心结果的情况下,通过使完成选项完全丢弃结果来保持默认的@noescape 行为。例如,当用户已经“走开”并且调用视图控制器不必仅仅因为有一些粗心的网络调用而挂在内存中。就像我来这里寻找答案时的情况一样,示例代码对我来说不是很相关,所以标记@noescape 并不是最好的选择,尽管乍一看它听起来是唯一的。

func fetchData(location: String, completion: ((DataFetchResult) -> Void)?) {
   ...
   completion?(self.result)
}

【讨论】:

    猜你喜欢
    • 2017-07-02
    • 2020-02-04
    • 2021-02-03
    • 2019-02-19
    • 2019-01-05
    • 2020-05-04
    • 2021-12-23
    • 2017-03-08
    • 2017-01-29
    相关资源
    最近更新 更多