---
title: 'Automatic Reference Counting'
source: 'https://academia.sh/en/courses/swift/automatic-reference-counting'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Reads the point where deinit runs after the last strong reference is released from the print order'
    - 'Measures the deinit moment at scope end, nil assignment, array removal and loop iteration'
    - 'Shows that a second reference postpones deinit until after the first release'
    - 'Measures that the deinit of an object held by a top-level let does not run'
---

# Automatic Reference Counting

Measures the point at which deinit runs once the last strong reference is released along four release paths, and shows that a second reference postpones that moment.

The previous lesson measured that an access level changes who can see a name but not the moment: a hidden `lazy` cache was built on first use even when it was `private`. This lesson moves from visibility to the question of when an object is destroyed. Class instances are managed by **automatic reference counting (ARC)**: the number of strong references to each instance is tracked, and the instance is destroyed at the moment that number drops to zero. While it is being destroyed, the class's `deinit` body runs. The question is the question of this course: when is the `deinit` body evaluated?

Earlier courses asked where a lifetime ends and who owns the memory. This lesson makes neither of those the category. The lifetime boundary and the owner are not measured; what is measured is after which line, and how many times, the `deinit` body runs, that is, the moment. The core comes first. `Probe` records how many times an expression was evaluated and whether that happened before or after use; `tally` counts the four moments.

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```


The class under measurement is `Session`. Its `deinit` body first prints a line and then notifies the owning `Probe` through `eval`. That way both the `print` order and the `Probe` counter show the same event.

```swift
// Session.swift — class that writes to a Probe in its deinit
final class Session {
    let name: String
    let owner: Probe

    init(_ name: String, owner: Probe) {
        self.name = name
        self.owner = owner
    }

    deinit {
        print("deinit \(name)")
        _ = owner.eval(name)
    }
}
```


## Four Release Paths

The first measurement releases a `Session` instance in four different ways. The first path is the end of a `do` scope. The second is assigning `nil` to an optional variable. The third is removing the instance from an array with `removeLast()`. The fourth is building a new instance in each iteration of a `for` loop. Each path runs at least twice, because the `Probe` rule asks for at least two uses for every observation other than `never`. The rule counts, it does not read intent.

```swift
// main.swift — which release runs deinit
let scopeEnd = Probe("scope-end")
let niled = Probe("nil-assignment")
let removed = Probe("array-removal")
let looped = Probe("loop-iteration")

for round in 1...2 {
    _ = scopeEnd.use {
        do {
            let temp = Session("scope-\(round)", owner: scopeEnd)
            print("used \(temp.name)")
        }
    }
}

for round in 1...2 {
    var held: Session? = Session("nil-\(round)", owner: niled)
    _ = niled.use { held = nil }
    print("after \(held == nil)")
}

var list = [Session("array-1", owner: removed), Session("array-2", owner: removed)]
_ = removed.use { list.removeLast() }
_ = removed.use { list.removeLast() }
print("remaining in array \(list.count)")

for round in 1...3 {
    let item = Session("loop-\(round)", owner: looped)
    _ = looped.use { item.name }
}

let probes = [scopeEnd, niled, removed, looped]
for probe in probes {
    probe.report()
}
tally(probes)
```

```
used scope-1
deinit scope-1
used scope-2
deinit scope-2
deinit nil-1
after true
deinit nil-2
after true
deinit array-2
deinit array-1
remaining in array 0
deinit loop-1
deinit loop-2
deinit loop-3
scope-end: 2 evaluations, 2 uses -> on-every-use
nil-assignment: 2 evaluations, 2 uses -> on-every-use
array-removal: 2 evaluations, 2 uses -> on-every-use
loop-iteration: 3 evaluations, 3 uses -> on-every-use
immediately: 0
on-first-use: 0
on-every-use: 4
never: 0
```


Start with the order of the `print` lines. The line `used scope-1` comes before `deinit scope-1`: in the measured run the instance was destroyed at the end of the scope, and the `deinit` line appeared on scope exit. With `nil` assignment, `deinit` runs inside the assignment, and the `after true` line that follows confirms it. The array holds two instances; `removeLast()` takes out the last one, so `array-2` is destroyed before `array-1`. In the loop, three iterations produce three separate `deinit` runs, and each one finishes before the next iteration.

All four `Probe` objects say `on-every-use`. The first three count two evaluations because there are two rounds, and the loop counts three because it has three rounds. The `deinit` body runs again every time an instance is destroyed, and every destruction belongs to a new instance. This lesson's moment is therefore **on every use**. In the `tally` output, `immediately`, `on-first-use` and `never` are zero.

## The Function Boundary

References are not held only by variables; an instance passed to a function as a parameter is also alive for the duration of the call. The second measurement separates two cases. In one, a function produces the instance and the caller holds it. In the other, the produced instance is bound to no variable and lives only in the middle of an expression.

```swift
// main.swift — a reference at the function boundary
func inspect(_ session: Session) -> String {
    session.name
}

func make(_ name: String, owner: Probe) -> Session {
    Session(name, owner: owner)
}

let crossing = Probe("function-boundary")

for round in 1...2 {
    let made = make("made-\(round)", owner: crossing)
    _ = crossing.use { inspect(made) }
    print("round done \(round)")
}

for round in 1...2 {
    _ = crossing.use { inspect(make("transient-\(round)", owner: crossing)) }
    print("transient round done \(round)")
}

crossing.report()
```

```
round done 1
deinit made-1
round done 2
deinit made-2
deinit transient-1
transient round done 1
deinit transient-2
transient round done 2
function-boundary: 4 evaluations, 4 uses -> on-every-use
```

In the first loop, the line `round done 1` comes before `deinit made-1`. The instance is alive during the `inspect` call, and the call does not extend its lifetime; the owner of the instance is still the `made` constant, and `deinit` runs at the end of the iteration, as `made` leaves its scope. In the second loop, the instance produced by `make` is bound to no name. In the measured run the instance was released as the calling expression finished, and the line `deinit transient-1` appears before `transient round done 1`. Together the two loops record four evaluations and four uses for `function-boundary`. In other words, passing an instance to a function does not extend its lifetime; what extends it is storing the instance in a name or a property.

## Chained Release

An instance can hold another instance in a property. In that case, destroying the outer instance also releases the last reference to the inner one. The third measurement builds two chains together: `Holder` holds a `Session`, and `Car` inherits from `Vehicle`, and both classes have a `deinit` body. The question is which `deinit` body runs first.

```swift
// main.swift — an instance in a property and a subclass chain
final class Holder {
    let name: String
    let child: Session

    init(_ name: String, owner: Probe) {
        self.name = name
        child = Session("child-\(name)", owner: owner)
    }

    deinit {
        print("deinit \(name)")
    }
}

class Vehicle {
    let owner: Probe

    init(owner: Probe) {
        self.owner = owner
    }

    deinit {
        print("deinit superclass")
        _ = owner.eval("super")
    }
}

final class Car: Vehicle {
    deinit {
        print("deinit subclass")
        _ = owner.eval("sub")
    }
}

let nested = Probe("property-instance")
let inherited = Probe("superclass-chain")

for round in 1...2 {
    var holder: Holder? = Holder("holder-\(round)", owner: nested)
    _ = nested.use { holder = nil }
}

for _ in 1...2 {
    var car: Car? = Car(owner: inherited)
    _ = inherited.use { car = nil }
}

nested.report()
inherited.report()
```

```
deinit holder-1
deinit child-holder-1
deinit holder-2
deinit child-holder-2
deinit subclass
deinit superclass
deinit subclass
deinit superclass
property-instance: 2 evaluations, 2 uses -> on-every-use
superclass-chain: 4 evaluations, 2 uses -> on-every-use
```

The output shows `deinit holder-1` first and then `deinit child-holder-1`: the outer instance's `deinit` body runs first, its properties are released as soon as that body finishes, and the inner instance's `deinit` follows. This is why the outer `deinit` body can still reach the inner instance. In the subclass chain, the subclass's `deinit` body runs first and the superclass's follows. `property-instance` counts two evaluations over two rounds, because only the inner `Session` calls it. `superclass-chain` sees two `eval` calls per round and so counts four evaluations over two rounds. Both say `on-every-use`: the number of evaluations can be greater than or equal to the number of uses, and the rule only looks at whether the expression ran more than once. `deinit` cannot be called explicitly and takes no parameters; it is run by the runtime exactly once, when the counter drops to zero.

## Overwriting Releases the Old Value

Assigning `nil` is not the only form of release. Assigning a new instance to a `var` property also releases the reference to the old instance the property was holding. This measurement assigns a second `Session` to the `session` property of a `Slot` class and prints `evaluations` on both sides of the assignment. The new instance is bound to a separate `Probe`, so the counted `deinit` belongs only to the old instance.

```swift
// main.swift — overwriting a property
final class Slot {
    var session: Session?

    init(_ session: Session?) {
        self.session = session
    }
}

let overwritten = Probe("overwrite")
let spare = Probe("spare")

for round in 1...2 {
    let slot = Slot(Session("old-\(round)", owner: overwritten))
    print("before write \(overwritten.evaluations)")
    _ = overwritten.use { slot.session = Session("new-\(round)", owner: spare) }
    print("after write \(overwritten.evaluations)")
}

overwritten.report()
```

```
before write 0
deinit old-1
after write 1
deinit new-1
before write 1
deinit old-2
after write 2
deinit new-2
overwrite: 2 evaluations, 2 uses -> on-every-use
```

In each round, `deinit old-1` appears between the lines `before write 0` and `after write 1`: while the assignment places the new value, it releases the last strong reference to the old value, and the old instance is destroyed before the assignment completes. `deinit new-1` comes at the end of the round, because `slot` leaves its scope at that point and releases the instance inside it. So the same property held two separate instances in sequence during the round and produced two separate `deinit` runs. `overwrite` counts two evaluations over two rounds and says `on-every-use`. The rule has not changed: `deinit` is not tied to a release line, it is tied to the counter dropping to zero. Overwriting subtracts one from that counter, and so do `nil` assignment and scope end.

## Bulk Clearing

The instances held by an array are not released one by one but as the array is emptied. This measurement builds two separate arrays and empties each with `removeAll()`. The question is how many `deinit` bodies a single call runs.

```swift
// main.swift — bulk release with removeAll
let cleared = Probe("bulk-clear")

for round in 1...2 {
    var batch = [
        Session("bulk-\(round)-a", owner: cleared),
        Session("bulk-\(round)-b", owner: cleared)
    ]
    print("before clear \(cleared.evaluations)")
    _ = cleared.use { batch.removeAll() }
    print("after clear \(cleared.evaluations), in array \(batch.count)")
}

cleared.report()
```

```
before clear 0
deinit bulk-1-a
deinit bulk-1-b
after clear 2, in array 0
before clear 2
deinit bulk-2-a
deinit bulk-2-b
after clear 4, in array 0
bulk-clear: 4 evaluations, 2 uses -> on-every-use
```

The `removeAll()` call is a single line, but because the array holds two instances, two `deinit` bodies run: between `before clear 0` and `after clear 2`, `deinit bulk-1-a` and `deinit bulk-1-b` appear. In the second round the counter goes from 2 to 4. Here `Probe` counts four evaluations for two uses and again says `on-every-use`; one call running several `deinit` bodies does not contradict the `on-every-use` rule, because the rule looks not at the number of evaluations but at whether the expression ran more than once. Each instance the array releases has its own counter. If one of them were also held by another variable, `removeAll()` would release only the array's share, and that instance's `deinit` would wait until its counter dropped to zero; the bounding measurement shows this case.

## The Bounding Measurement

The rule above may give the impression that `deinit` runs at every release. The last measurement shows that this is wrong. Two variables, `first` and `second`, hold the same instance. First `first` is released, then `second`. `shared.evaluations` is read after each step.

```swift
// main.swift — a second reference and a top-level let
let shared = Probe("second-reference")
let outlives = Probe("top-level-let")

for round in 1...2 {
    var first: Session? = Session("shared-\(round)", owner: shared)
    var second = first
    _ = shared.use { first = nil }
    print("first released: \(shared.evaluations) deinit, second \(second == nil ? "absent" : "present")")
    _ = shared.use { second = nil }
    print("second released: \(shared.evaluations) deinit")
}

let pinned = Session("top-level", owner: outlives)
_ = outlives.use { pinned.name }
_ = outlives.use { pinned.name }

shared.report()
outlives.report()
```

```
first released: 0 deinit, second present
deinit shared-1
second released: 1 deinit
first released: 1 deinit, second present
deinit shared-2
second released: 2 deinit
second-reference: 2 evaluations, 4 uses -> on-every-use
top-level-let: 0 evaluations, 2 uses -> never
```

After the first release the `deinit` count is zero, and `second` still holds the instance. The line `deinit shared-1` appears only at the second release. The same sequence repeats in the second round. The counter reaching zero is the condition for `deinit` to run; the release line itself is not the condition. The `second-reference` `Probe` records two evaluations and four uses and says `on-every-use`, but it does not say this alone: which release triggers `deinit` can be seen only from the `print` order and the `evaluations` reads placed in between.

The last two lines show the second limit. `pinned`, held by a top-level `let`, is used twice, but the `top-level-let` `Probe` records zero evaluations and says `never`. A top-level `let` is held until the end of the process, and in this measurement `deinit` does not run at process end. The absence of `deinit` is not an error; the reference count never dropped to zero. Relying on the `deinit` body of such an instance to close a resource would be a mistake.

**MA1.** Only one instance was measured for each release path; that the rule also holds on other release paths was not shown in this lesson. Choosing four paths is a design choice.

## Summary

- Class instances are managed by automatic reference counting (ARC); `deinit` runs at the moment the last strong reference is released.
- Scope end, `nil` assignment, array removal and loop iteration follow the same rule: `deinit` runs at the release point.
- A second strong reference postpones `deinit`; `deinit` does not run until the counter drops to zero.
- An instance held by a top-level `let` is not released until the end of the process, and in this measurement `deinit` does not run (`never`).
- This lesson's moment: **on every use**. The destruction of each new instance runs the `deinit` body again.

## Next Step

In this lesson the counter had to drop to zero, and it did. If two classes hold each other strongly, the counter never drops to zero and neither `deinit` runs. The next lesson measures how `weak` and `unowned` break this cycle, and why the `deinit` body is never evaluated in it.
