在转义闭包中究竟如何捕获结构?

How exactly are structs captured in escaping closures?

在我看来,转义闭包会通过复制捕获结构是合乎逻辑的。但如果是这样的话,下面的代码就没有意义,不应该编译:

struct Wtf {
    var x = 1
}

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

然而它编译成功,甚至在调用 foo 时打印 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

如果 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)")
}

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

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