非逃逸参数的闭包使用可能导致其逃逸。

189

我有一个协议:

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不再起作用。您能告诉我根本原因吗?


5
这是一篇非常精彩的文章,解释了为什么Swift 3要以这种方式进行操作。 - mfaani
1
我们不得不这样做毫无意义。其他语言都不需要这样做。 - Andrew Koster
3个回答

324

这是由于函数类型参数默认行为的更改导致的。在 Swift 3 之前(具体来说是随 Xcode 8 beta 6 发布的版本),它们会默认为 escaping - 您需要标记 @noescape 才能防止它们被存储或捕获,从而保证它们不会超出函数调用的持续时间。

然而,现在 @noescape 已经成为函数类型参数的默认值。如果您想要存储或捕获这样的函数,现在需要将它们标记为 @escaping

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

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

查看Swift Evolution提案以获取有关此更改的更多信息。


2
但是,你如何使用闭包以防止逃逸? - Eneko Alonso
6
@EnekoAlonso,我不太确定你在问什么 - 你可以直接在函数本身中调用非逃逸函数参数,或者在非逃逸闭包中捕获后调用它。在这种情况下,由于我们正在处理异步代码,无法保证 async 函数参数(因此是 completion 函数)会在 fetchData 退出之前被调用 - 因此必须使用 @escaping - Hamish
感觉很丑陋,我们必须在协议的方法签名中指定@escaping...这是我们应该做的吗?提案没有说!:S - Sajjon
1
@Sajjon 目前,您需要将协议要求中的 @escaping 参数与该要求的实现中的 @escaping 参数进行匹配(对于非逃逸参数也是如此反之亦然)。在 Swift 2 中,@noescape 也是如此。 - Hamish
@EnekoAlonso 请查看 https://developer.apple.com/documentation/swift/2827967-withoutactuallyescaping - Peter Schorn

46

由于@noescape是默认设置,有两种选项来修复错误:

1)如@Hamish在他的答案中指出的那样,如果您关心结果并真正希望它进行转义(这可能是@Lukasz提出的问题,在使用单元测试作为示例和异步完成的可能性时),只需将完成标记为@escaping即可。

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

或者

2) 通过使完成可选并在您不关心结果的情况下完全放弃结果来保持默认的@noescape行为。例如,当用户已经“离开”并且调用视图控制器不必挂起在内存中时,就可以这样做,因为有一些粗心的网络调用。就像我来找答案时样本代码对我来说不是很相关,所以标记@noescape不是最好的选择,即使从第一眼看起来似乎是唯一的选择。

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

0
将完成块作为可选变量,帮助我解决了Swift 5中的问题。
 private func updateBreakTime(for id: String, to time: Time, onSucess: EmptyAction?) {
    dataRepository.updateBreak(
        id: id,
        to: time.seconds,
        onSuccess: { _ in
            onSucess?()
        },
        onError: { [weak self] error in
            self?.screen.showError(error)
        }
    )
}

在 dataRepository 的 onSuccess 中使用了 @escaping


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接