I ran into some (possibly) unrelated issues while working on my software engineering assignment in Swift, and decided to explore the topic of closures and capture lists, particularly while working with reference types.
Bob wrote an excellent article on closures and capture lists here, and I don’t want to repeat any of that. Instead, I wanted to compare the use of value and reference types with closures and capture lists.
Closures with value types
To begin, closures capture references to variables. This is the starting point.
The above prints:
1: 5 has address: 0x000056174d7dc1d8
2: 5 has address: 0x000056174d7dc1d8
2: 6 has address: 0x000056174d7dc1d8
Even though we run the same closure twice, the output is different, because the variable x has been incremented between the closure runs.
Closures with reference types
What if we reference an instance of a class (reference type) from within the closure then? Something like this:
I’m using the function
withUnsafePointer() to peek at the memory addresses. A few observations about this snippet:
- Both memory addresses are identical. That’s expected, since closures capture references to variables. There’s no reason to expect this behavior to change when working with reference types instead of value types.
- The output of the value of foo.x differs between runs of the same closures. Again, completely expected (this is normal reference type behavior).
Capture lists with value types
To “lock” the value of a variable into a closure and have it be insulated from subsequent reassignments, we could use capture lists.
Some modifications are required:
- I reassigned
var y = xin the closure to work around the error “cannot pass immutable value as inout argument”.
- I remove the
\(x)portion of my print string to avoid the error “overlapping accesses to ‘x’, but modification requires exclusive access”.
- The output of the value of x is constant at 5 (this is expected behavior).
- The memory address printed from within the closure is different from the “original” x variable. This is because a local copy is created. This part is unsurprising.
- The memory addresses printed from the first closure run is different from all the other closure runs (this part is very surprising). Try tacking on
runClosure(closure1)a few more times after line 24. They all print the same memory address. Only the memory address printed in line 20 is different. I don’t know why.
Capture lists with reference types
I tried doing the same with reference types. I included an additional print statement, to show that
fooCopy === foo (i.e. that fooCopy is a non-immutable variable that points to the same underlying object as foo).
Observation and lesson: You can “shallow-lock” a reference type using a capture list into a closure, but not “deeper” references. Therefore, in this case, the value of foo.x changes between closure runs, even though foo has been “locked in” (shallowly) through the capture list.