Swift 3.0 错误:转义闭包只能按值显式捕获 inout 参数

Posted

技术标签:

【中文标题】Swift 3.0 错误:转义闭包只能按值显式捕获 inout 参数【英文标题】:Swift 3.0 Error: Escaping closures can only capture inout parameters explicitly by value 【发布时间】:2016-09-19 08:56:55 【问题描述】:

我正在尝试将我的项目更新到 Swift 3.0,但我遇到了一些困难。 我收到下一个错误:“转义闭包只能按值显式捕获 inout 参数”。

问题出在这个函数内部:

fileprivate func collectAllAvailable(_ storage: inout [T], nextUrl: String, completion: @escaping CollectAllAvailableCompletion) 
    if let client = self.client 
        let _ : T? = client.collectionItems(nextUrl) 

            (resultCollection, error) -> Void in

            guard error == nil else 
                completion(nil, error)
                return
            

            guard let resultCollection = resultCollection, let results = resultCollection.results else 
                completion(nil, NSError.unhandledError(ResultCollection.self))
                return
            

            storage += results // Error: Escaping closures can only capture inout parameters explicitly by value

            if let nextUrlItr = resultCollection.links?.url(self.nextResourse) 

                self.collectAllAvailable(&storage, nextUrl: nextUrlItr, completion: completion) 
                // Error: Escaping closures can only capture inout parameters explicitly by value

             else 
                completion(storage, nil) 
                // Error: Escaping closures can only capture inout parameters explicitly by value
            
        
     else 
        completion(nil, NSError.unhandledError(ResultCollection.self))
    

有人可以帮我解决这个问题吗?

【问题讨论】:

@Hamish 感谢您的帮助。那么这意味着我们不能在异步任务中使用 inout 吗?另外,我不确定,但我认为它适用于 Swift 2.2。异步工作存储后应该等于新值。 我已经删除了我的cmets,并将它们编译为您的答案。 @Hamish 非常感谢!问题解决了,我应该使用可变副本。 乐于助人 :) 在这种情况下,您甚至不需要可变副本 - 您可以制作不可变副本,并将 results 附加到 storage(即 let storage = storage + results)上,正如我在回答中所展示的那样。 【参考方案1】:

inout 参数专门用于异步任务是对inout 的滥用——因为在调用函数时,传递给inout 参数的调用者的值不会改变。

这是因为inout 不是按引用传递,它只是在函数退出时写回调用者的参数的可变影子副本——并且因为异步函数立即退出,所以不会发生任何更改写回来。

您可以在下面的 Swift 2 示例中看到这一点,其中 inout 参数允许被转义闭包捕获:

func foo(inout val: String, completion: (String) -> Void) 
    dispatch_async(dispatch_get_main_queue()) 
        val += "foo"
        completion(val)
    

var str = "bar"
foo(&str) 
    print($0) // barfoo
    print(str) // bar

print(str) // bar

因为传递给dispatch_async 的闭包逃逸了函数foo 的生命周期,所以它对val 所做的任何更改都不会写回到调用者的str 中——这种更改只能从被观察到传递给完成函数。

在 Swift 3 中,inout 参数不再允许被 @escaping 闭包捕获,这消除了预期传递引用的混乱。相反,您必须通过复制来捕获参数,将其添加到闭包的capture list:

func foo(val: inout String, completion: @escaping (String) -> Void) 
    DispatchQueue.main.async [val] in // copies val
        var val = val // mutable copy of val
        val += "foo"
        completion(val)
    
    // mutate val here, otherwise there's no point in it being inout

(编辑: 自发布此答案以来,inout 参数现在可以编译为传递引用,可以通过查看发出的 SIL 或 IR 来查看。但是你是由于无法保证 whatsoever 调用者的值在函数调用后仍将保持有效,因此无法这样对待它们。)


但是,在您的情况下,根本不需要inout。您只需要将请求中的结果数组附加到您传递给每个请求的当前结果数组中。

例如:

fileprivate func collectAllAvailable(_ storage: [T], nextUrl: String, completion: @escaping CollectAllAvailableCompletion) 
    if let client = self.client 
        let _ : T? = client.collectionItems(nextUrl)  (resultCollection, error) -> Void in

            guard error == nil else 
                completion(nil, error)
                return
            

            guard let resultCollection = resultCollection, let results = resultCollection.results else 
                completion(nil, NSError.unhandledError(ResultCollection.self))
                return
            

            let storage = storage + results // copy storage, with results appended onto it.

            if let nextUrlItr = resultCollection.links?.url(self.nextResourse) 
                self.collectAllAvailable(storage, nextUrl: nextUrlItr, completion: completion) 
             else 
                completion(storage, nil) 
            
        
     else 
        completion(nil, NSError.unhandledError(ResultCollection.self))
    

【讨论】:

作为“知识饥渴”到您的帖子的扩展 - 这个宣言 github.com/apple/swift/blob/master/docs/OwnershipManifesto.md 包含一个很好的评论 inout 参数引入 Swift 优化的问题。有一个很好的解释为什么inout参数传递引用不能被这样对待。 这行代码:DispatchQueue.main.async [val] in 对我不起作用。我正在使用 Swift 3。 @VanDuTran“不起作用”如何?编译器错误?出乎意料的结果? @Hamish 编译错误:“转义闭包只能通过值显式捕获 inout 参数”。 @VanDuTran 我只能说我的答案中的第三个代码块在 Swift 3 中编译得很好。你必须展示你的特定问题的minimal reproducible example - 请提出一个新问题(说明您已经看过此问答,但它并没有解决您的特定问题)。【参考方案2】:

如果要修改转义闭包中通过引用传递的变量,可以使用 KeyPath。这是一个例子:

class MyClass 
    var num = 1
    func asyncIncrement(_ keyPath: WritableKeyPath<MyClass, Int>) 
        DispatchQueue.main.async 
            // Use weak to avoid retain cycle
            [weak self] in
            self?[keyPath: keyPath] += 1
        
    

您可以查看完整示例here。

【讨论】:

好主意。 KeyPath 是强类型的,所以它也很安全。【参考方案3】:

如果您确定您的变量将始终可用,只需使用真正的指针(与 inout 实际所做的相同)

func foo(val: UnsafeMutablePointer<NSDictionary>, completion: @escaping (NSDictionary) -> Void) 
    val.pointee = NSDictionary()
    DispatchQueue.main.async 
        completion(val.pointee)
    

【讨论】:

以上是关于Swift 3.0 错误:转义闭包只能按值显式捕获 inout 参数的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 3.0 中的转义闭包中改变自我(结构/枚举)

转义闭包捕获 Swift 中的变异“self”参数错误

如何解决转义闭包捕获 Swift 中的“inout”参数?

Swift 5:转义闭包捕获'inout'参数

错误:转义闭包捕获变异的“自我”参数

转义闭包捕获非转义参数“函数” Xcode 说