非转义参数的闭包使用可能允许它转义

我有个规定:

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。你能告诉我根本原因吗?

119414 次浏览

这是由于函数类型参数的默认行为发生了变化。在 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) {
// ...
}

有关此更改的更多信息,请参见 迅速进化计划

因为@noescape 是默认的,所以有两个选项来修复这个错误:

1)正如@Hamish 在他的回答中指出的那样,如果你真的关心结果并且真的希望它能够转义,那么只需要将完成标记为@escape (在@Lukasz 的问题中,单元测试作为例子和异步完成的可能性就是这种情况)

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

或者

2)保持默认的@noescape 行为,在你不关心结果的情况下,让完成可选地丢弃所有的结果。例如,当用户已经“离开”,调用视图控制器不必挂起内存,只是因为有一些粗心的网络调用。正如我来这里寻找答案时遇到的情况一样,样本代码对我来说并不是很重要,所以标记@noescape 并不是最好的选择,尽管乍一看它似乎是唯一的选择。

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