Capturing values
You can hand over a recipe card with a note clipped to it: "two spoons of salt are already in". The note goes along with the card, and it gets updated as you go: one more spoon goes in, and you correct the number on the note.
You already know that a closure sees the constants and variables around it. But what happens if the function that declared a variable has already finished? Let's look at a counter:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
let other = makeCounter()
print(other()) // 1
print(counter()) // 3The header func makeCounter() -> () -> Int reads like this: a function without parameters returns a function without parameters, and that one returns an Int. The closure after return has no parameters, so its header with in can be left out: the compiler takes its type () -> Int from the header of makeCounter. Let's go through the program:
makeCounter()creates the variablecountwith the value0, returns a closure and finishes. The closure goes intocounter.- Each call
counter()increasescountby one and returns it: first1, then2. - The second call
makeCounter()creates a new variablecount, and the count ofotherstarts at1. - The last
counter()continues its own count and prints3: the call tootherdid not touch it.
Capturing
When a closure uses a variable from the surrounding code, we say that it captures it. Capturing works like this:
- the closure holds the variable itself, not a copy of its value, so the next call sees each change to
count; - a captured variable lives as long as the closure lives, even though the call to
makeCounterfinished long ago; - each call to
makeCountercreates a newcount, socounterandothereach keep their own count.
Note that counter is a let constant, yet the count still grows. let forbids putting another closure into counter, but it does not stop the closure from changing the captured count. count itself is declared with var; otherwise it could not change.
Press "Next step" and watch the "captured" row: the makeCounter frame disappears, while count stays with the closure and grows.
1func makeCounter() -> () -> Int {2 var count = 03 return {4 count += 15 return count6 }7}8 9let a = makeCounter()10print(a())11print(a())12let b = makeCounter()13print(b())
The program calls makeCounter and waits for what it returns.
As long as the counter lives, its variable lives too. Nothing outside can reach count: the only way to change the count is to call the counter itself.
A closure is a reference type
An array is a value type: after var second = first you have two independent arrays. A closure is different:
let same = counter
print(same()) // 4
print(counter()) // 5Let's go through it line by line:
let same = countercreates no new counter and does not copycount. Now one closure has two names.same()continues the same count: before thatcounter()returned3, so now it is4.counter()sees this change and returns5.
A type whose assignment and passing to a function do not copy the value but give one more name to the same thing is called a reference type. Closures in Swift are a reference type.
Here the note picture stops working. You can photocopy a card with its note, and the copy gets a note of its own. But let same = counter makes no copy: both names lead to one card and one note.
Interviewers like to ask this: are closures a value type or a reference type? The answer:
- a reference type: after
let same = counterboth names lead to one closure; - they share the captured variables, and a change through one name is seen through the other.
Common mistake
The count variable was declared inside the closure:
func makeCounter() -> () -> Int {
return {
var count = 0 // ✗
count += 1
return count
}
}
let counter = makeCounter()
print(counter(), counter()) // 1 1There is no error, but the count does not grow. Now the line var count = 0 runs on every call of the closure, and every call starts from zero. Declare count in the body of makeCounter, before return: then there is one variable for the whole counter, and the closure captures it.
Key points
- A closure captures the variables it uses: it holds the variable itself, not a copy of its value.
- A captured variable lives as long as the closure lives, even after the call that declared it has ended.
- Each call to
makeCountercreates a new variable and a new counter. - A closure is a reference type: after
let same = counterboth names share one closure and its variables.