Modelling with types · core

Classes and reference semantics

When shared identity is the point — and the short list of reasons to prefer a class over a struct.

11 min read13 min practice0/3 exercises4 recall cards

By the end you will be able to

  • Explain the difference between value and reference semantics with a concrete example
  • Use inheritance, overriding and `super` correctly
  • Apply the decision rule for choosing struct or class
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

Same code as the last lesson's pretest, but Point is now a class. What prints?

class Point { var x = 0; var y = 0 }
let a = Point()
let b = a
b.x = 99
print(a.x)

One object, many names

class Account {
    var balance: Double = 0
}

let shared = Account()
let alias = shared
alias.balance = 100
print(shared.balance)   // 100 — same object

A class instance lives in one place, and variables hold references to it. Copying a variable copies the reference, not the object.

That is sometimes exactly right. A bank account, a database connection, a view controller, a download in progress — these are things where "the same one" is a meaningful idea and multiple parts of the program genuinely should see the same state.

Swift

=== asks "are these the same object?" — a question that only makes sense for reference types. == asks "are these equal?", which you define yourself.

Classes need initializers

Structs get a memberwise initializer for free. Classes do not — you write one, and it must assign every stored property that has no default. The missing synthesis is inheritance's fault: if Download gained a subclass, a synthesised memberwise init would have to either ignore the subclass's new properties (leaving them silently unset behind an init that looks complete) or rewrite its own signature every time the superclass changed a stored property. Two bad programs — so Swift refuses to pick one, and asks you to write the initialization story down.

class Account {
    let owner: String
    var balance: Double

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

self.owner = owner disambiguates the property from the parameter of the same name. This is idiomatic and worth getting comfortable with.

Inheritance

Only classes can inherit.

class Vehicle {
    var wheels: Int { 0 }
    func describe() -> String { "A vehicle with \(wheels) wheels" }
}

class Bicycle: Vehicle {
    override var wheels: Int { 2 }
}

class Car: Vehicle {
    override var wheels: Int { 4 }
    override func describe() -> String {
        super.describe() + ", which is a car"
    }
}
  • override is required. Accidentally shadowing a superclass member is a compile error, not a subtle bug.
  • super calls the superclass version.
  • final prevents further overriding, and lets the compiler devirtualise the call.
Swift

Notice the last loop: every element is typed Vehicle, yet each one runs its own describe. That is dynamic dispatch — the method chosen at runtime by the object's actual type.

Deinitializers

A class can run cleanup when its last reference goes away:

class FileHandle {
    deinit {
        print("closing the file")
    }
}

Structs have no deinit, because there is no single moment when a value "goes away" — copies of it may still exist.

Which should you use?

Swift's own guidance, and the practical rule:

Use a struct by default. Switch to a class when one of these is true:

  1. Identity matters. Two objects with identical contents are still meaningfully different things — two open network connections, two rows in a UI that must be tracked individually.
  2. You need shared mutable state. Several parts of the app must see the same changes.
  3. You need inheritance, or must subclass an Apple framework class (UIViewController, NSObject).
  4. You need deinit to release a non-memory resource.
  5. The type wraps something that is already a reference, such as a file handle or a Core Data object.

Everything else — models, configuration, coordinates, view state, network payloads — is a struct.

Struct: a value
struct Money {
    var amount: Decimal
    var currency: String
}

let a = Money(amount: 10, currency: "GBP")
let b = a
// b is a separate £10.
// Asking "is it the same £10?"
// is not a meaningful question.
Class: a thing
class Download {
    var bytesReceived = 0
    var isPaused = false
}

let a = Download()
let b = a
// b IS the download.
// Pausing through either name
// pauses the same operation.

The test: does "the same one" mean something for this type? £10 is £10 — there is no identity. A download in progress is a specific ongoing thing you can pause. That question answers struct-or-class more reliably than any performance argument.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

An array of classes, copied into a second variable. What does this print?

class Box {
    var value: Int
    init(_ value: Int) { self.value = value }
}

var first = [Box(1), Box(2)]
var second = first

second.append(Box(3))
second[0].value = 99

print(first.count)
print(second.count)
print(first[0].value)
Check yourself

You are modelling a user profile fetched from an API: id, name, avatar URL. Struct or class?

Shared vs copied

Write the code

Complete both types so the printed output demonstrates the difference between value and reference semantics.

Solution unlocks after 3 attempts

Inherit and override

Write the code

Build a small shape hierarchy where each subclass reports its own area, and print each shape's description.

Solution unlocks after 3 attempts

Spot the shallow copy

Write the code

Fix duplicate(_:) so the returned array holds independent boxes rather than references to the originals.

Solution unlocks after 3 attempts
RecallSaved on this device. Never graded.

Without looking back: list the five reasons to choose a class over a struct, then check yourself against the lesson. Note which ones you missed — those are the ones to re-read.

Checkpoint

You can now:

  • Explain value versus reference semantics with a concrete example
  • Use inheritance, override, super and final
  • State why let means different things for structs and classes
  • Apply the struct-by-default rule and name the exceptions

Next up: enums — which in Swift can carry data, and are the tool for making illegal states impossible to represent.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.