I would say it's more important to think about storage locations than scopes. E.g., consider:
var x = 0
var maybeClosure: (() -> Void)?
do {
maybeClosure = { print(x) }
}
x = 1
maybeClosure?() // 1
Even though the change happens outside the scope where the closure is defined, we're still able to observe the change.
If we instead think about storage locations, we can actually generalize everything we've seen under one principle: closures always capture variables by reference. The various behaviors we've seen so far that make some captures appear to happen by value are actually the result of us creating new storage locations at some step along the way.
First, let's consider capture lists. When I write:
var x = 0
let closure = { [x] in print(x) }
x = 1
closure() // 0
the compiler actually transforms this so that it is identical to writing:
var x = 0
let closure = { [x = x] in print(x) }
x = 1
closure() // 0
which is further equivalent to something like (departing from actually valid Swift syntax):
var x = 0
let closure = { [let x = x] in print(x) }
x = 1
closure() // 0
That is, when you insert a variable into the capture list, you're actually declaring a new variable with the same name as the original variable, and capturing the new variable. In fact, the way that capture lists are written inside the closure is somewhat misleading. The semantics are actually more like:
var x = 0
let closure: () -> Void
do {
let x = x
closure = { print(x) }
}
x = 1
closure() // 0
I.e., the new x gets initialized to the value of the old x at the point when the closure is declared, not when the closure is later run (as can be seen from the behavior in the examples).
Similarly, when you capture a function parameter (or self, since self is just an implicit parameter):
func formClosure(_ x: Int) -> () -> Void {
{ print(x) }
}
var x = 0
let closure = formClosure(x)
x = 1
closure() // 0
You're capturing the parameter x to formClosure, which does not necessarily have the same storage location as the argument x that we passed in. This should be clear from the fact that we can rename the parameter:
func formClosure(_ y: Int) -> () -> Void {
{ print(y) }
}
var x = 0
let closure = formClosure(x)
x = 1
closure() // 0
The fact that closure() here prints 0 should be no more surprising than the fact that the following prints 0 as well:
var x = 0
let y = x
let closure = { print(y) }
x = 1
closure() // 0