Modelling with types · core

Structs and value semantics

The type you should reach for by default, and the copying behaviour that removes an entire category of bug.

11 min read14 min practice0/3 exercises4 recall cards

By the end you will be able to

  • Define structs with stored properties, computed properties and methods
  • Explain what `mutating` means and why structs need it
  • Predict the result of assigning or passing a struct
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

Point is a struct. What does this print?

struct Point { var x: Int; var y: Int }
var a = Point(x: 1, y: 2)
var b = a
b.x = 99
print(a.x, b.x)

Defining a struct

struct Temperature {
    var celsius: Double

    var fahrenheit: Double {
        celsius * 9 / 5 + 32
    }

    func isFreezing() -> Bool {
        celsius <= 0
    }

    mutating func warm(by degrees: Double) {
        celsius += degrees
    }
}

Four things are on display:

  • Stored propertycelsius holds a value.
  • Computed propertyfahrenheit calculates on each access; it stores nothing.
  • Method — an ordinary function that can see self.
  • Mutating method — one that changes the struct, which needs saying out loud.
Swift

The memberwise initializer, for free

You never wrote init above, yet Temperature(celsius: 18) worked. Swift synthesises a memberwise initializer for every struct, taking each stored property in declaration order. var properties with default values become optional arguments (a let with a default is settled — it is left out of the initializer entirely):

struct Settings {
    var theme: String = "dark"
    var fontSize: Int = 14
}

Settings()                        // both defaults
Settings(fontSize: 18)            // theme defaults
Settings(theme: "light", fontSize: 12)

Write your own init when you need validation or a more convenient signature.

mutating — why it exists

A method on a struct cannot change the struct unless it is marked mutating. This looks like ceremony until you see what it enables:

let fixed = Temperature(celsius: 20)
// fixed.warm(by: 5)   ← does not compile

Because fixed is a let, Swift refuses to call a mutating method on it. The compiler is telling you that a constant is genuinely constant — not "the reference is constant but the contents can change underneath you". mutating is what makes that check possible.

Under the hood, a mutating method receives self as an inout parameter. That is also why a mutating method can replace the whole value:

mutating func reset() {
    self = Temperature(celsius: 0)
}

Property observers

willSet and didSet run around a change to a stored property:

Swift

newValue and oldValue are provided automatically. Observers do not fire during initialization — only on later changes. Picture the refused world: a didSet firing mid-init would run your reaction code against a half-built value — reading a sibling property that has not been assigned yet — and every freshly constructed value would announce a phantom "change" nobody made. Observers watch changes to a whole value; during init there is no whole value yet to change.

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

Structs are copied when passed to functions. What does this print?

struct Counter {
    var count = 0
    mutating func increment() {
        count += 1
    }
}

func bumpTwice(_ counter: Counter) -> Counter {
    var copy = counter
    copy.increment()
    copy.increment()
    return copy
}

var original = Counter()
original.increment()

let result = bumpTwice(original)

print(original.count)
print(result.count)

Static members

static attaches something to the type rather than an instance:

struct Temperature {
    static let absoluteZero = Temperature(celsius: -273.15)
    static func fromFahrenheit(_ f: Double) -> Temperature {
        Temperature(celsius: (f - 32) * 5 / 9)
    }
}

Temperature.absoluteZero
Temperature.fromFahrenheit(98.6)

Static properties are computed lazily on first use and only once, which makes them the standard way to express constants and shared configuration.

Check yourself

Why does Swift require the mutating keyword on struct methods that change properties?

Model a shopping basket

Write the code

Build a Basket struct with an items array of (name, price) tuples, a computed total, and a mutating add method.

Solution unlocks after 3 attempts

Validate in an initializer

Write the code

Give Percentage a custom initializer that clamps any input into 0…100.

Solution unlocks after 3 attempts

Observe a change

Write the code

Add a didSet observer to progress that prints done exactly when it reaches 100 — and not on any other change.

Solution unlocks after 3 attempts
Design itSaved on this device. Never graded.

Sketch a struct for a calendar event: a title, a start time, a duration, and an optional location. Decide which properties are let and which are var, and write one sentence for each explaining why.

Checkpoint

You can now:

  • Define structs with stored properties, computed properties, methods and observers
  • Explain mutating and predict when the compiler will reject a call
  • Predict what assigning or passing a struct does

Next up: classes — reference semantics, inheritance, and how to decide which of the two you actually need.

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.