Skip to content
Theory · Closures · lesson 7 of 8

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:

SWIFT
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()) // 3

The 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:

  1. makeCounter() creates the variable count with the value 0, returns a closure and finishes. The closure goes into counter.
  2. Each call counter() increases count by one and returns it: first 1, then 2.
  3. The second call makeCounter() creates a new variable count, and the count of other starts at 1.
  4. The last counter() continues its own count and prints 3: the call to other did 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 makeCounter finished long ago;
  • each call to makeCounter creates a new count, so counter and other each 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.

LiveTwo countersstep 1 of 12 · frames: 1

The program calls makeCounter and waits for what it returns.

The "captured" row shows the variables the closures hold

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:

SWIFT
let same = counter
print(same()) // 4
print(counter()) // 5

Let's go through it line by line:

  1. let same = counter creates no new counter and does not copy count. Now one closure has two names.
  2. same() continues the same count: before that counter() returned 3, so now it is 4.
  3. counter() sees this change and returns 5.

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 = counter both 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:

SWIFT
func makeCounter() -> () -> Int {
    return {
        var count = 0 // ✗
        count += 1
        return count
    }
}

let counter = makeCounter()
print(counter(), counter()) // 1 1

There 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 makeCounter creates a new variable and a new counter.
  • A closure is a reference type: after let same = counter both names share one closure and its variables.