3

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?

itotsev
  • 93
  • 1
  • 7
  • 1
    Interestingly, `inout`parameters, which are similar to a local `var` in many ways cannot escape into an escaping closure. – Sweeper Feb 07 '22 at 19:35
  • 1
    `inout` parameters includes a "copy back" step when the function returns. That can't be performed if you capture them. The "inout" term is quite literal. It means "the value is copied in, and then at the end of the function it is copied out." It does not mean "create a local reference." – Rob Napier Feb 07 '22 at 20:02
  • @RobNapier But in my case, isn't the captured instance of `wtf` also copied back to the instance declared in foo stack frame (so that x can become 5 there)? That should not be possible if the stack frame is no longer there when the block finishes executing asynchronously. – itotsev Feb 07 '22 at 20:21
  • 1
    No. It's not copied back at all. When you capture a variable, it is moved to the heap, so that it won't be lost when the stack frame goes away. Both inside and outside the closure, `wtf` is bound to the same thing, however. As your code demonstrates, there are no copies. – Rob Napier Feb 07 '22 at 20:22
  • Also shouldn't the reasoning be similar to this case https://stackoverflow.com/questions/39702990/mutating-self-struct-enum-inside-escaping-closure-in-swift-3-0 . I'm also mutating a struct from an escaping closure (but not from one of its methods). – itotsev Feb 07 '22 at 20:25
  • 1
    Oh definitely. You absolutely shouldn't be doing this. It's undefined behavior (you're not allowed to manipulate structs on two threads simultaneously). But the compiler is not smart enough to detect it. Eventually it will be. See the Ownership Manifesto for more on the future plans. (But if this were a thread-safe object like a lock, then this could be legal and useful, so it's somewhat complex for the compiler to know for certain if this is wrong.) – Rob Napier Feb 07 '22 at 20:27
  • 1
    `mutating` and `inout` are easier for the compiler to reason about, and it can provide an error. But if this closure weren't executed on another thread, there might be nothing wrong with it. (For example if `wtf` were a struct property on a class.) – Rob Napier Feb 07 '22 at 20:32

1 Answers1

2

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.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • 1
    Ok. It's a bit clearer now. Seems to me that in terms of good old Objective C blocks, it's like all local variables have an implicit __block qualifier in swift. But I can't say I like this. This behavior is nice for non-escaping closures. But the escaping ones give me nightmares... I don't see a practical reason to use anything like your example with the counters. And my example with dispatching the closure asynchronously is a recipe for disaster. – itotsev Feb 07 '22 at 20:47
  • If I create a new ‘wtf’ and reassign back to existing ‘wtf’ var _anytime_ before the GCD async function runs, the closure knows of the new changes of this var. Is it safe to say that even value types are captured internally to the heap (and referenced) such that: if this main function updates the ‘wtf’ var value itself or when the main function returns, the closure is still able to hold a strong reference to it and update/print the ‘wtf’ value? I couldn’t find some documentation on this.. – CyberMew Aug 09 '22 at 03:08