Classes and reference semantics

Swift · Essentials · 12 min

UIKit, many Apple frameworks and observable models are classes, and reference semantics cause classic bugs.

A class looks like a struct but is a reference type: assigning or passing it copies a reference, so all variables point to the same instance. Changing it through one variable is visible through every other. Compare identity with ===.

Classes differ from structs in a few ways:

  • No memberwise initializer; every stored property must be initialized in an init (or have a default).
  • Inheritance: class Dog: Animal. Override methods and properties with override, call the parent with super. Mark a class or member final to forbid overriding, which also lets the compiler devirtualize calls.
  • Methods can change properties without mutating, even through a let reference — let only fixes which object the constant points to.
  • Deinitializers (deinit) run when the last strong reference goes away.
  • Memory is managed by automatic reference counting (ARC).

Initialization has rules: a designated initializer must set all of the subclass's own properties before calling super.init, and a convenience initializer must call another initializer of the same class. required init forces subclasses to implement it.

Use a class when an object has a lifecycle or identity that several parts of the program share (a network session, a view model, a cache). Mark it final unless you design it for subclassing.

Example

class Account {
    let owner: String
    private(set) var balance: Double = 0
    init(owner: String) { self.owner = owner }
    func deposit(_ amount: Double) { balance += amount }
    func describe() -> String { "\(owner): \(balance)" }
}

final class SavingsAccount: Account {
    let rate: Double
    init(owner: String, rate: Double) {
        self.rate = rate               // own properties first
        super.init(owner: owner)
    }
    override func describe() -> String { super.describe() + " @\(rate)" }
}

let a = SavingsAccount(owner: "Lea", rate: 0.02)
let alias = a                         // same instance
alias.deposit(100)
print(a.describe())                   // Lea: 100.0 @0.02
print(a === alias)                    // true

Remember

  • Classes are reference types; === checks identity
  • Subclass init sets its own properties before super.init
  • Mark classes final unless designed for inheritance

Watch out for

  • Passing a class instance to several places and being surprised that a change shows up everywhere.

Check yourself

  1. `let acc = Account(owner: "A")` — is `acc.deposit(10)` allowed, given deposit changes balance?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/