转义闭包中的结构到底是如何被捕获的?

ito*_*sev 4 struct closures swift

对我来说,转义闭包将通过复制捕获结构似乎是合乎逻辑的。但如果是这种情况,下面的代码就没有意义,不应该编译:

struct Wtf {
    var x = 1
}

func foo(){
    
    var wtf = Wtf()
    
    DispatchQueue.global().async {
        wtf.x = 5
    }
    
    Thread.sleep(forTimeInterval: 2)
    print("x = \(wtf.x)")
}
Run Code Online (Sandbox Code Playgroud)

然而它编译成功,甚至在调用 foo 时打印 5。这怎么可能?

Rob*_*ier 5

虽然复制结构可能有意义,正如您的代码所示,但事实并非如此。这是一个强大的工具。例如:

func makeCounter() -> () -> Int {
    var n = 0
    return {
        n += 1  // This `n` is the same `n` from the outer scope
        return n
    }

    // At this point, the scope is gone, but the `n` lives on in the closure.
}

let counter1 = makeCounter()
let counter2 = makeCounter()

print("Counter1: ", counter1(), counter1())  // Counter1:  1 2
print("Counter2: ", counter2(), counter2())  // Counter2:  1 2
print("Counter1: ", counter1(), counter1())  // Counter1:  3 4
Run Code Online (Sandbox Code Playgroud)

如果n被复制到闭包中,这是行不通的。重点是闭包捕获并可以修改其自身外部的状态。这就是闭包(“关闭”创建它的范围)和匿名函数(不关闭)的区别。

(术语“封闭”的历史有点晦涩。它指的是 lambda 表达式的自由变量已“封闭”的想法,但 IMO“绑定”将是一个更明显的术语,也是我们描述的方式其他地方都是如此。但是“闭包”这个词已经使用了几十年,所以我们在这里。)

请注意,可以获得复制语义。你只需要要求它:

func foo(){

    var wtf = Wtf()

    DispatchQueue.global().async { [wtf] in // Make a local `let` copy
        var wtf = wtf   // To modify it, we need to make a `var` copy
        wtf.x = 5
    }

    Thread.sleep(forTimeInterval: 2)
    // Prints 1 as you expected
    print("x = \(wtf.x)")
}
Run Code Online (Sandbox Code Playgroud)

在 C++ 中,lambda 必须明确说明如何通过绑定或复制来捕获值。但在 Swift 中,他们选择将绑定设置为默认值。

至于为什么wtf在被闭包捕获后还允许访问,这只是 Swift 中缺乏移动语义。今天的 Swift 中没有办法表达“这个变量已经被传递给其他东西,并且可能不再在此范围内被访问”。这是该语言的一个已知限制,并且需要做大量工作来修复它。有关更多信息,请参阅所有权宣言