Swift – Local variables in a closure

1.1k views Asked by At

I'm studying Swift's closures but I have some questions about closure capture lists. I know that closures are a reference type. I know the meaning of reference type and value type. But in a closure, when they capture the values (local variables of closures), how can it be that local variables are reference type?

For example:

var i: Int = 0

var closureArray: [() -> ()] = []

for _ in 1...5 {
    closureArray.append { print(i) }
    i += 1
}

I think this is the most famous example of capture list in closure. Then I thought i is the integer type and it is also value type but how can i be the reference type in closure blocks?

Is this the same concept with scope chain in JavaScript? Can anyone explain it clearly?

2

There are 2 answers

0
Chip Jarred On

While a closure itself is a reference type, the local variables it captures are not necessarily references. If you think of a closure conceptually as an executable instance of a class that has the captured variables as properties, you're surprising close to the way they are implemented (unless they are marked @convention(c) in which case they are just function pointers, but in that case, they can't capture anything). All of this is subject to optimization, such as inlining.

Of course, if the local variable is itself a reference type, for example, an instance of a class, it is captured by reference.

Immutable local value type variables are captured by value though. For example, if your code were:

for i in 1...5 {
    closureArray.append { print(i) }
}

closureArray.forEach { $0() }

You'd get

1
2
3
4
5

Mutable local value type variables are captured by reference, which works in a way very similar to inout parameters to functions. Under the hood, what is actually captured is a pointer to the mutable value.

Originally, I said that such closures cannot be @escaping, and I'm pretty sure that was true at one point, but your question prompted me to do an experiment that shows otherwise:

func foo() -> [() -> ()]
{
    var i: Int = 0
    var closureArray: [() -> ()] = []

    for _ in 1...5 {
        closureArray.append { print(i); i += 1 }
    }
    
    return closureArray
}

foo().forEach { $0() }

The closures escape the function scope via the returned Array, and yet capture the local variable, i, by reference. Despite the closures being run after foo returns, the output is:

0
1
2
3
4

I even tried running a series of deeply nested functions between getting to thoroughly smash the stack between obtaining the closure array from foo and calling the closures, and it had no effect on the output.

It seems that it's boxing i rather than actually putting it on the stack. This implies that Swift boxes the local variable as well (ie. when used in foo outside of the closure). That makes sense, because it allows the closure to safely be executed, though it does introduce some potential performance degradation since the box has to be dynamically allocated. I put the code into Compiler Explorer. This is the Intel Assembly for the line var i: Int = 0:

        add     rdi, 16
        mov     esi, 24
        mov     edx, 7
        call    swift_allocObject@PLT
        mov     qword ptr [rbp - 160], rax
        mov     rcx, rax
        add     rcx, 16
        mov     qword ptr [rbp - 152], rcx
        mov     qword ptr [rbp - 16], rcx
        mov     qword ptr [rax + 16], 0

Note the call to swift_allocObject@PLT. That's dynamically allocating the box for i. So the thing that foo and the closure both use when they access i is a pointer to that allocated memory.

As a comparison, if I remove the closures altogether:

func foo()
{
    var i: Int = 0

    for _ in 1...5 {
        print(i); i += 1
    }
    
}

Now the Assembly line corresponding to var i: Int = 0 is just one instruction:

        mov     qword ptr [rbp - 16], 0

So capturing a mutable variable not only affects how the variable is accessed in the closure, but also in the scope in which that variable was declared.

I don't program in JavaScript, so I'm not qualified to compare captured variables in Swift with any feature of JavaScript.

0
Andy Jazz On

Semantics in Swift

It's true, Swift closures are reference types. However, Swift closures capture the variables themselves – neither the variables' values nor the values of the references which those variables point to. Therefore, we shouldn't care what semantics i has.

About scope chain in JavaScript

Scope chain means that one variable has a scope (it may be global or local/function or block scope) is used by another variable or function having another scope (may be global or local/function or block scope). This complete chain formation goes on and stops when the user wishes to stop it according to the requirement.