itotsev
itotsev

Reputation: 121

How exactly are structs captured in escaping closures?

It seems logical to me that escaping closures would capture structs by copying. But if that was the case, the following code makes no sense and should not compile:

struct Wtf {
    var x = 1
}

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

Yet it compiles successfully and even prints 5 when foo is called. How is this possible?

Upvotes: 5

Views: 1296

Answers (1)

Rob Napier
Rob Napier

Reputation: 299345

While it might make sense for a struct to be copied, as your code demonstrates, it is not. That's a powerful tool. For example:

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

If n were copied into the closure, this couldn't work. The whole point is the closure captures and can modify state outside itself. This is what separates a closure (which "closes over" the scope where it was created) and an anonymous function (which does not).

(The history of the term "close over" is kind of obscure. It refers to the idea that the lambda expression's free variables have been "closed," but IMO "bound" would be a much more obvious term, and is how we describe this everywhere else. But the term "closure" has been used for decades, so here we are.)

Note that it is possible to get copy semantics. You just have to ask for it:

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

In C++, lambdas have to be explicit about how to capture values, by binding or by copying. But in Swift, they chose to make binding the default.

As to why you're allowed to access wtf after it's been captured by the closure, that's just a lack of move semantics in Swift. There's no way in Swift today to express "this variable has been passed to something else and may no longer be accessed in this scope." That's a known limitation of the language, and a lot of work is going into fix it. See The Ownership Manifesto for more.

Upvotes: 6

Related Questions