Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7. Structs, Classes, and Value Semantics

Swift gives you two ways to define a type. A struct is a value: assigning it copies it. A class is an object: assigning it makes a second name for the same thing. Everything else in this chapter follows from that one sentence, and so does most of the difference between a Swift program that behaves and one that does not.

The languages you are coming from mostly do not offer the choice. In Python and JavaScript every object of your own making is a reference, and the copying kind exists only for numbers and strings. Swift puts the decision in your hands and expects you to take the value side by default.

7.1 The Difference, Demonstrated

// The same program twice: once with a struct, once with a class.
struct PointValue {
    var x: Int
    var y: Int
}

final class PointObject {
    var x: Int
    var y: Int
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

var a = PointValue(x: 1, y: 1)
var b = a
b.x = 99
print("struct:", a.x, b.x)

let c = PointObject(x: 1, y: 1)
let d = c
d.x = 99
print("class: ", c.x, d.x)
print("same object:", c === d)
struct: 1 99
class:  99 99
same object: true

b = a made a second point. d = c made a second name. That is the entire distinction, and every other difference between the two is a consequence of it.

Two consequences show up in the code above. The struct got an initializer for free, generated from its stored properties, while the class had to be given one, because a subclass might add properties the memberwise form would not know about. And c and d are declared let, yet d.x = 99 is allowed: the constant is the reference, not the object it points at. For a struct, let means the value cannot change at all.

That last point is where a let in Swift stops resembling const in C. A let struct is deeply immutable, down through every struct and array and string inside it. A let class reference is a constant arrow pointing at something that anybody holding it can rearrange.

7.2 Structs

A struct has stored properties, computed properties, and methods. A method that changes a stored property must say mutating, which tells the caller as well as the compiler.

// A struct with stored properties, computed properties, and mutating methods.
struct Account {
    let owner: String
    private(set) var balance: Int
    var history: [String] = []

    var isOverdrawn: Bool {
        balance < 0
    }

    var summary: String {
        "\(owner): \(balance) cents" + (isOverdrawn ? " (overdrawn)" : "")
    }

    init(owner: String, opening: Int = 0) {
        self.owner = owner
        self.balance = opening
    }

    mutating func deposit(_ amount: Int) {
        balance += amount
        history.append("+\(amount)")
    }

    mutating func withdraw(_ amount: Int) {
        balance -= amount
        history.append("-\(amount)")
    }
}

var acct = Account(owner: "ada", opening: 100)
acct.deposit(50)
acct.withdraw(200)

print(acct.summary)
print(acct.history)

let frozen = acct
acct.deposit(1000)
print(frozen.balance, acct.balance)
ada: -50 cents (overdrawn)
["+50", "-200"]
-50 950

frozen was taken before the last deposit and still holds what the account looked like then. Nothing was copied by hand and nothing can reach back and change it. That is the property this chapter is selling: a value handed to somebody else is a fact about a moment, not a window onto whatever happens next.

A computed property is a function that reads like a field. isOverdrawn and summary store nothing and run each time they are read, and the single expression is the return value the way it is in a one-line function. Add a set block beside the get and it becomes writable; write only the body and you have a read-only property, which is the common case.

private(set) makes balance readable from anywhere and writable only inside the type, which is how you offer a fact without offering a lever. Access control in full is private, fileprivate, internal (the default, meaning the whole module), package, and public, plus open for classes that may be subclassed outside their module. Chapter 11 says where module boundaries come from.

Writing any initializer of your own suppresses the free memberwise one. Put your initializer in an extension instead and you keep both, which is a trick worth knowing early.

The two errors you will make are these:

missing_mutating.swift:3:21: error: left side of mutating operator isn't mutable: 'self' is immutable
3 |     func bump() { n += 1 }
  |     |               `- error: left side of mutating operator isn't mutable: 'self' is immutable
  |     `- note: mark method 'mutating' to make 'self' mutable
mutating_let.swift:7:9: error: cannot use mutating member on immutable value: 'counter' is a 'let' constant
6 | let counter = Counter()
  | `- note: change 'let' to 'var' to make it mutable
7 | counter.bump()

The second is the first one’s point arriving at the call site. A mutating method is one that replaces the value, so it may only be called on something that is allowed to be replaced.

Property observers run on assignment to a stored property:

var level: Int = 0 {
    willSet { print("about to become \(newValue)") }
    didSet { if level > 10 { level = 10 } }
}

They are useful for clamping and for keeping a derived value in step, and they are a place people put too much behavior. A didSet that calls three other things is a method wearing a disguise.

static members belong to the type rather than an instance, which is where constants and factory functions go. static let shared is also how a singleton is spelled, for the rare case that is the right answer.

7.3 Classes

Use a class when the thing you are modeling has an identity that is separate from its contents. Two accounts with the same balance are the same value; two network connections with the same configuration are not the same connection. That is the test, and it decides most cases on its own.

The other reasons are narrower. You need inheritance, and a subclass really is a substitutable specialization rather than a shortcut for sharing code. You need a deinit to release something at a known moment. You are talking to a framework that requires one.

Classes bring inheritance, override, super, and the initializer rules that go with them: a designated initializer must set every property it declares and then call super.init, a convenience initializer must delegate across to another in the same class. Mark a class final when nothing should subclass it, which is most classes, and the compiler can then call its methods directly rather than through a table.

=== asks whether two references point at the same object, which is a question that only makes sense for classes and is why == is not the same thing.

Reference counting

Swift frees an object when the last reference to it goes away. The counting is inserted by the compiler rather than done by a background collector, so deallocation happens at a predictable point and deinit runs then.

The failure mode is a cycle. Two objects that refer to each other keep each other’s count above zero, and neither is ever freed.

// Two objects pointing at each other, and the word that lets them go.
final class Room {
    let name: String
    var occupant: Guest?
    init(_ name: String) { self.name = name }
    deinit { print("freed room \(name)") }
}

final class Guest {
    let name: String
    var room: Room?
    init(_ name: String) { self.name = name }
    deinit { print("freed guest \(name)") }
}

final class Suite {
    let name: String
    var occupant: Tenant?
    init(_ name: String) { self.name = name }
    deinit { print("freed suite \(name)") }
}

final class Tenant {
    let name: String
    weak var suite: Suite?
    init(_ name: String) { self.name = name }
    deinit { print("freed tenant \(name)") }
}

func leaks() {
    let room = Room("101")
    let guest = Guest("ada")
    room.occupant = guest       // strong
    guest.room = room           // strong, and now neither can ever reach zero
    print("leaving leaks()")
}

func doesNot() {
    let suite = Suite("102")
    let tenant = Tenant("grace")
    suite.occupant = tenant     // strong
    tenant.suite = suite        // weak, so the count never rises
    print("leaving doesNot()")
}

leaks()
print("---")
doesNot()
print("---")
leaving leaks()
---
leaving doesNot()
freed suite 102
freed tenant grace
---

leaks() prints no deinit at all. The room and the guest are unreachable the moment the function returns and they are still in memory when the program ends. Nothing warns you; the only symptom is that a long-running program grows.

A weak reference does not raise the count. It is optional and automatically becomes nil when the object goes away, which is why weak var and never weak let. unowned also does not raise the count and is not optional, so it is faster and crashes if you are wrong; use it only when the referred-to object is guaranteed to outlive the referrer, as a child pointing back at a parent that owns it.

The rule that covers nearly every case: the owner holds strong, the owned holds back weakly. A parent owns its children; a child refers weakly to its parent. A closure stored on an object and referring to that object needs [weak self] in its capture list, which is the single most common cycle in real code and the reason Swift makes escaping closures spell out self.

7.4 Copying Is Cheaper Than It Sounds

If arrays are values, and an array of a million elements is assigned, has a million elements been copied? No. Swift’s collections share their storage and copy it only when somebody writes to one of the copies.

// Copying an array is free until somebody writes to one of the copies.
func storage(_ a: [Int]) -> UInt {
    a.withUnsafeBufferPointer { UInt(bitPattern: $0.baseAddress) }
}

var a = [1, 2, 3]
var b = a
print("after assignment, shared:", storage(a) == storage(b))

b.append(4)
print("after b changed, shared:", storage(a) == storage(b))
print(a, b)
after assignment, shared: true
after b changed, shared: false
[1, 2, 3] [1, 2, 3, 4]

That is copy-on-write, and String, Array, Dictionary and Set all do it. You get the behavior of copying, which is simple to reason about, and you pay for a copy only where one is needed. A struct of your own holding those types inherits the trick for free, since the copy of the struct copies the references inside it and the storage stays shared.

You can build the same behavior into your own type with isKnownUniquelyReferenced(&storage), which is what you do when wrapping a class-based buffer in a value-shaped API. It is a rare need and the standard library has done it for you everywhere it counts.

7.5 Choosing

Start with struct. Reach for class when identity matters, when you need inheritance, or when a framework demands it. This is the reverse of the default in most object-oriented languages and it is the advice Apple’s own API design guidance gives.

What you get for it is local reasoning. A function that takes a struct cannot change the caller’s copy, so reading the call tells you what can happen without reading the function. Two threads holding the same value are holding two values and cannot race over it, which Chapter 12 leans on heavily. And a value stored in a dictionary or captured in a closure is the value as it was, not a window onto whatever it becomes.

What you give up is shared mutable state, which you wanted less often than you think.

Exercise 7-1. Add a mutating func transfer(_:to:) to Account taking the other account as inout. It compiles. Now call it with the same account on both sides, read the error, and say what it is protecting.

Exercise 7-2. Change PointValue to a class in copyshare.swift and find every line of output that changes.

Exercise 7-3. Write a Deck struct holding an array of cards, with a mutating func shuffle(). Then hand a deck to a function that shuffles it and confirm the caller’s deck is untouched.

Exercise 7-4. Fix leaks() by making one of the two references weak, and decide which one it should be.

Exercise 7-5. Add a closure property to Room that prints the room’s name, assign it inside init without a capture list, and confirm from the deinit output that the object now leaks by itself.