Lesson 20 / 24
Strong, Weak and Unowned References
Measures that deinit never runs when two classes hold each other strongly, that weak and unowned break the cycle, and that an unowned reference cannot be read once its object is gone.
Published Updated
In this lesson
- Recognizes a retain cycle formed by two classes holding each other strongly from the deinit output
- Breaks the cycle by making one reference weak and observes the weak reference become nil when its object is gone
- Binds an unowned reference to the side that lives longer
- Distinguishes weak from unowned by reading after the object is gone
Contents
In the previous lesson the reference count dropped to zero and deinit ran. The count reaching zero was a condition, not a guarantee. If two class instances hold each other with strong references, each one keeps the other’s counter one above zero, and both stay alive even when every outside name has been released. This situation is called a retain cycle. This lesson builds the cycle, breaks it in two ways, and measures when the deinit body runs in each.
There are two tools for breaking it. A weak reference (weak) does not hold the instance: it does not raise the counter, and it becomes nil on its own when the instance it points to is destroyed, so it is always an optional var. An unowned reference (unowned) also does not raise the counter, but it never becomes nil and does not have to be optional; in return it asks for a promise that the instance it points to will outlive the holder. Probe is given as in the previous lesson.
// 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 Retain Cycle
The two measured classes are Account and Profile. The deinit body of each prints a line and notifies the shared Probe through eval. Account holds a Profile, and Profile holds an Account; both use the default, that is, strong, reference. In each round two instances are built, linked to each other, and then both are set to nil. The outside names are released this way.
// main.swift — two classes hold each other strongly
final class Account {
let name: String
let owner: Probe
var profile: Profile?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
deinit {
print("deinit account \(name)")
_ = owner.eval(name)
}
}
final class Profile {
let title: String
let owner: Probe
var account: Account?
init(_ title: String, owner: Probe) {
self.title = title
self.owner = owner
}
deinit {
print("deinit profile \(title)")
_ = owner.eval(title)
}
}
let cyclic = Probe("retain-cycle")
for round in 1...2 {
var account: Account? = Account("account-\(round)", owner: cyclic)
var profile: Profile? = Profile("profile-\(round)", owner: cyclic)
account?.profile = profile
profile?.account = account
_ = cyclic.use {
account = nil
profile = nil
}
print("released \(round): \(cyclic.evaluations) deinit")
}
cyclic.report()
released 1: 0 deinit released 2: 0 deinit retain-cycle: 0 evaluations, 2 uses -> never
In both rounds the deinit count after the release is zero, and no deinit line ever appears. Because each instance is held by the other’s property, both counters stay at one or higher. retain-cycle records two uses and zero evaluations, and the rule says never. This lesson’s moment is therefore never: the deinit body is not evaluated at any moment. Nothing reports an error, either. The two instances in the cycle stay in memory until the end of the program.
Breaking It with a Weak Reference
To break the cycle, the reference on only one of the two sides is made weak. The second measurement defines the Profile.account property as weak var and changes nothing else. This time the instances are released in two steps: first account, then profile. After each step profile?.account is read and the deinit count is printed.
// main.swift — one side holds weakly
final class Account {
let name: String
let owner: Probe
var profile: Profile?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
deinit {
print("deinit account \(name)")
_ = owner.eval(name)
}
}
final class Profile {
let title: String
let owner: Probe
weak var account: Account?
init(_ title: String, owner: Probe) {
self.title = title
self.owner = owner
}
deinit {
print("deinit profile \(title)")
_ = owner.eval(title)
}
}
let broken = Probe("weak-reference")
for round in 1...2 {
var account: Account? = Account("account-\(round)", owner: broken)
var profile: Profile? = Profile("profile-\(round)", owner: broken)
account?.profile = profile
profile?.account = account
print("linked: \(profile?.account == nil ? "nil" : "set")")
_ = broken.use { account = nil }
print("account released: \(profile?.account == nil ? "nil" : "set"), \(broken.evaluations) deinit")
_ = broken.use { profile = nil }
print("profile released: \(broken.evaluations) deinit")
}
broken.report()
linked: set deinit account account-1 account released: nil, 1 deinit deinit profile profile-1 profile released: 2 deinit linked: set deinit account account-2 account released: nil, 3 deinit deinit profile profile-2 profile released: 4 deinit weak-reference: 4 evaluations, 4 uses -> on-every-use
After linking, profile?.account is set. When the account name is released, no strong reference to the Account instance remains: because profile.account is weak, it had not raised the counter. The line deinit account account-1 appears immediately, and the read that follows gives nil; a weak reference becomes nil on its own at the moment the instance it points to is destroyed. The Profile instance, on the other hand, is still held by the profile name, so it stays alive, and the deinit profile line appears only when that name is released. Four deinit bodies ran in two rounds, and weak-reference says on-every-use.
Breaking It with an Unowned Reference
A weak reference says that the instance may disappear. If a relationship does not run in that direction, that is, if a Card will never exist without a Customer, the reference is written as unowned let. The third measurement builds this relationship: Customer holds an optional card, and Card carries an unowned let customer. The customer holds the card, and the card does not hold the customer. The measurement releases only the customer name.
// main.swift — an unowned reference binds to the longer-lived side
final class Customer {
let name: String
let owner: Probe
var card: Card?
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
deinit {
print("deinit customer \(name)")
_ = owner.eval(name)
}
}
final class Card {
let number: String
let owner: Probe
unowned let customer: Customer
init(_ number: String, customer: Customer, owner: Probe) {
self.number = number
self.customer = customer
self.owner = owner
}
deinit {
print("deinit card \(number)")
_ = owner.eval(number)
}
}
let bound = Probe("unowned-reference")
for round in 1...2 {
var customer: Customer? = Customer("customer-\(round)", owner: bound)
if let current = customer {
current.card = Card("card-\(round)", customer: current, owner: bound)
print("card owner: \(current.card?.customer.name ?? "none")")
}
_ = bound.use { customer = nil }
print("released \(round): \(bound.evaluations) deinit")
}
bound.report()
card owner: customer-1 deinit customer customer-1 deinit card card-1 released 1: 2 deinit card owner: customer-2 deinit customer customer-2 deinit card card-2 released 2: 4 deinit unowned-reference: 4 evaluations, 2 uses -> on-every-use
In every round the deinit customer line comes before the deinit card line. When the customer is released, its counter drops to zero, the deinit body runs, its properties are then released, and the card’s only strong reference goes with them. What breaks the cycle in this chain is that the card does not hold the customer strongly; unowned expresses this without making the reference optional. unowned-reference records two uses and four evaluations, two deinit bodies run in every round, and the rule says on-every-use.
The Delegate Pattern
The most common place for a weak reference is the delegate pattern: an object stores a second object that it will notify when its work is finished, while the notified object already holds the object doing the work. Strong references in both directions would build a retain cycle. This measurement uses the weak var delegate property of the Worker class, and Reporter holds the Worker strongly. At the end of the round only the reporter name is released, and the worker name stays in hand.
// main.swift — the delegate is weak, the worker is strong
final class Reporter {
let name: String
let owner: Probe
let worker: Worker
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
worker = Worker()
worker.delegate = self
}
deinit {
print("deinit reporter \(name)")
_ = owner.eval(name)
}
}
final class Worker {
weak var delegate: Reporter?
}
let handedOver = Probe("delegate")
for round in 1...2 {
var reporter: Reporter? = Reporter("reporter-\(round)", owner: handedOver)
let worker = reporter!.worker
print("delegate \(worker.delegate?.name ?? "nil")")
_ = handedOver.use { reporter = nil }
print("released: delegate \(worker.delegate?.name ?? "nil"), \(handedOver.evaluations) deinit")
}
handedOver.report()
delegate reporter-1 deinit reporter reporter-1 released: delegate nil, 1 deinit delegate reporter-2 deinit reporter reporter-2 released: delegate nil, 2 deinit delegate: 2 evaluations, 2 uses -> on-every-use
After linking, worker.delegate gives the name of the reporter. When the reporter name is released, no strong reference to the Reporter instance remains: because Worker holds it weakly, it had not raised the counter. The line deinit reporter reporter-1 appears immediately, and the read that follows gives nil. The worker keeps living because it is held by the worker name; although Reporter held it in a property, Reporter itself is gone, so the Worker’s only remaining share is the outside name. The pattern relies on this asymmetry: the notified object does not have to outlive the notifier, and the notifier learns that the notified object is gone by reading nil. This is why a delegate property is always written as an optional value and checked before every use. Two deinit bodies run in two rounds, and delegate says on-every-use.
Weak and Unowned Reads
As long as the instance they point to is alive, the two reference forms give the same value. The difference begins after the instance is gone. This measurement builds a Watcher: two properties, one a weak var and the other an unowned let, point to the same Target instance. While the instance is alive both are read; then the instance is released and only the weak property is read. The unowned property is not read after this point; the read would stop the program.
// main.swift — a weak and an unowned view of the same target
final class Target {
let name: String
let owner: Probe
init(_ name: String, owner: Probe) {
self.name = name
self.owner = owner
}
deinit {
print("deinit target \(name)")
_ = owner.eval(name)
}
}
final class Watcher {
weak var soft: Target?
unowned let strict: Target
init(_ target: Target) {
soft = target
strict = target
}
}
let looked = Probe("weak-and-unowned")
for round in 1...2 {
var target: Target? = Target("target-\(round)", owner: looked)
let watcher = Watcher(target!)
print("while alive: weak \(watcher.soft?.name ?? "nil"), unowned \(watcher.strict.name)")
_ = looked.use { target = nil }
print("after gone: weak \(watcher.soft?.name ?? "nil")")
}
looked.report()
while alive: weak target-1, unowned target-1 deinit target target-1 after gone: weak nil while alive: weak target-2, unowned target-2 deinit target target-2 after gone: weak nil weak-and-unowned: 2 evaluations, 2 uses -> on-every-use
While the instance is alive, both reads give target-1; at this moment the two forms cannot be told apart. After the line deinit target target-1, the weak read gives nil: once the instance is destroyed, a weak reference reads as nil. The unowned read is not measured at the same point: the strict.name line would read an instance that is gone, and the runtime would stop the program. The choice between the two forms is therefore not a choice of reading style but a promise about what must hold. If the target can go before the watcher, write weak; if that is impossible, write unowned. weak-and-unowned counts two evaluations in two rounds and says on-every-use; the result of the weak read is determined anew at every read. The result of an earlier read is not carried to a later one.
The Bounding Measurement
Weak and unowned references have only this in common: they do not hold what they point to. They part ways when read after the instance they point to is gone. A weak reference gives nil, and the second measurement above showed this. An unowned reference has no such guarantee: reaching an instance that is gone through unowned stops the program at run time. This is why that read is not measured. The fragment below is source only and is not run. It makes no output claim either.
// Source fragment: not run
var customer = Customer("customer-3", owner: bound)
let card = Card("card-3", customer: customer, owner: bound)
customer = Customer("customer-4", owner: bound)
print(card.customer.name)
The third line releases the last strong reference to customer-3; because Card does not hold it, the instance is destroyed. The fourth line reads an instance that has been destroyed. Had the same structure been written with weak, the read would have given nil and the program would have continued. The choice is therefore a lifetime promise: unowned is bound only to an instance that is certain to live longer than the instance carrying the reference; wherever there is doubt, weak is used. The compiler does not check this promise, so this is a limit, not a moment.
MA2. One class pair was measured for each linking form; all three pairs are small chains of two classes. That the same rule holds in longer cycles (three or more classes) was not shown in this lesson. Choosing the pairs is a design choice.
Summary
- If two classes hold each other strongly, their reference counts never drop to zero and neither
deinitruns; this is a retain cycle. - Making one side
weak varbreaks the cycle; a weak reference becomesnilwhen the instance it points to is destroyed. unowned letis bound to the side that lives longer and never becomesnil; reading an instance that has been destroyed stops the program, so it is not measured.- This lesson’s moment: never. In a retain cycle the
deinitbody is not evaluated at any moment; once the cycle is broken it runs in every round.
Next Step
In this lesson the cycle was built between two class properties. A closure stored in a class’s own property can build the same cycle by capturing self strongly, and this is the most common form of retain cycle. The next lesson measures capturing in closures and when the [weak self] capture list is evaluated.
To keep your progress and take notes, Log in
My notes
Log in to take notes.