Some things to note about capturing in Swift Closure
Closure, a reference type, is among the most powerful features in Swift. We often use them daily in various ways, such as passing them as arguments to other functions, dispatching tasks asynchronously, and more. Another capability we are familiar with is capturing values, which will be discussed in this memo.
Capturing in Closure
Capturing is a powerful feature, but it can be tricky at times, especially for beginners. To understand capturing, let's first explore what "capture" means.
As we may already know, Closures (or Closure Expressions) are the foundational form of Functions. Functions are essentially special cases of closures with fully defined names, parameter types, return types, and more. Closures have the unique ability to "capture and store references to any constants and variables from the context in which they are defined." More importantly, Swift manages all the memory management involved in capturing for us. Cool!
However, that doesn't mean we can use it freely without any consideration! We might have encountered this common mistake when using closure capturing with self, like this:
It's clear enough to understand what's happening. The body of the "closure" has "captured" the self of "Foo", creating a cycle that cannot be broken automatically. We need to gain more insight into capturing to avoid mistakes and to harness its power for greater development efficiency.
The example above is just one form of capturing. In fact, there is another form, which involves "nested functions", as shown below:
Each function defines a scope or context containing its body and parameters. In other words, the "inner function" defines an "inner context", just as the "outer function" defines an "outer context". As defined by Apple's documentation:
A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.
Returning to our example above, the runningTotal belongs to the "outer context" or the context of the makeIncrementer function, and it was captured by the "inner context". This allows the inner incrementer function to freely modify this captured variable within its body. But is that enough? There is actually a more important aspect related to the "lifetime" of that captured variable. We all know that a function’s scope, along with its local variables, will terminate after it returns. Thanks to the capturing capability, we still have access to them. This is how Swift effectively manages memory for us.
But have you ever wondered if this "refer to and modify" capability of capturing applies to value types? We are definitely familiar with "refer to and modify" reference types such as Classes. However, in the above example, our runningTotal variable is a value type of Int! We know that value types are typically copied when passed around, such as when used as function arguments or even just when assigning the value to another variable, like this:
That's what makes closures different. In fact, runningTotal is considered a "local variable", and it is not copied when captured from the same or an inner scope where it was defined.
So, how do we "force" it to be copied? The answer lies in using Capture Lists. Let's compare the two examples below:
In the first example, the variable a is freely modifed by the foo closure.
In the second example, the "inner" a becomes a constant after being defined inside the capture list. This new a is a copy of the original a. Therefore, any modifications to the original a won't affect the copied one.
Utilizing Capturing for state management
Using for state manipulation inside immutable value type
In situations where we need to manage state within a value type such as a Struct, it can be tempting to do something like this:
It's clear that we are not allowed to do so. Instead, we can modify it slightly:
Of course, this approach is limited, as the currentValue state variable is only used within the current closure or other closures. If we need broader state manipulation, such as using it as a property, we might need to consider other approaches, such as using Class or Actor for better thread safety.
However, this approach is quite handy, especially when used with Combine, like this:
In conclusion, closures in Swift offer powerful capabilities for capturing and managing state, but they come with nuances that require careful consideration. Understanding how closures capture variables, especially in the context of value types versus reference types, is crucial for efficient and error-free code.
References: https://www.swiftbysundell.com/articles/swifts-closure-capturing-mechanics/ https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures#Escaping-Closures