Skip to content

1. Structs & classes

What & why

Swift lets you define your own types. The two workhorses are structs (value types) and classes (reference types). Choosing between them is the most important design decision in everyday Swift — and the answer is "start with a struct."

Structs

struct Point {
    var x: Double
    var y: Double
}

let origin = Point(x: 0, y: 0)       // free "memberwise initializer"
var p = Point(x: 3, y: 4)
p.x += 1
print(p.x, p.y)                      // 4.0 4.0

A struct bundles stored properties and (as we'll see) methods. Swift synthesizes an initializer taking every property.

Value semantics

Assigning or passing a struct copies it. Each variable owns its own independent value:

var a = Point(x: 1, y: 1)
var b = a
b.x = 99
print(a.x)    // 1.0 — a is unaffected
print(b.x)    // 99.0

This is the same behaviour you've seen with Int, String, Array, and Dictionary — they are all structs.

let on a struct makes the whole value immutable:

let fixed = Point(x: 1, y: 1)
// fixed.x = 5     // ERROR: cannot assign to property: 'fixed' is a 'let' constant

Classes

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

let c = Counter()       // `let` is fine — the reference is constant, the object isn't
c.increment()
c.increment()
print(c.count)          // 2

Reference semantics

Assigning or passing a class instance copies only the reference; both names point at the same object:

let first = Counter()
let second = first      // same object
second.increment()
print(first.count)      // 1 — changed through the other name
print(first === second) // true — identity operator: same instance?

Side-by-side

struct class
Semantics value (copied) reference (shared)
Inheritance no yes (single inheritance)
Memberwise init automatic you write init
Mutation of let instance not allowed allowed (properties)
Identity (===) none yes
Deinitializer (deinit) no yes
Thread-safety story simple (copies) needs care

Initializers

Custom initializers go in the type body:

struct Temperature {
    var celsius: Double

    init(celsius: Double) {
        self.celsius = celsius
    }

    init(fahrenheit: Double) {
        self.celsius = (fahrenheit - 32) * 5 / 9
    }
}
let t = Temperature(fahrenheit: 212)
print(t.celsius)       // 100.0

Tip: if you define an init inside the struct body, you lose the free memberwise one. To keep both, put your custom init in an extension (next lesson).

Failable initializers return an optional:

struct Email {
    let address: String
    init?(_ text: String) {
        guard text.contains("@") else { return nil }
        address = text
    }
}
print(Email("a@b.com") != nil)   // true
print(Email("nope") != nil)      // false

Default property values reduce boilerplate:

struct Settings {
    var volume = 50
    var isMuted = false
}
let s = Settings(volume: 80)     // isMuted uses its default

Class initializers

Classes require every stored property to have a value before init finishes, and they have no memberwise init:

class Player {
    let name: String
    var health: Int

    init(name: String, health: Int = 100) {
        self.name = name
        self.health = health
    }
}

Mutating methods

A struct method that changes the struct must be marked mutating:

struct Stack {
    private var items: [Int] = []

    mutating func push(_ x: Int) { items.append(x) }
    mutating func pop() -> Int? { items.popLast() }
    func peek() -> Int? { items.last }
    var count: Int { items.count }
}

var stack = Stack()
stack.push(1)
stack.push(2)
print(stack.pop() ?? -1)   // 2

Classes don't need mutating because they're modified through a reference.

Which should I choose?

Default to struct. Use a class when you need:

  • Shared mutable identity — one object that many parts of your program observe or update (a network session, a database connection, a cache).
  • Inheritance or Objective-C interop (NSObject subclasses).
  • A deinit to release a resource when the last reference goes away.

Value types are easier to reason about: no spooky action at a distance, and safe to pass between threads. Most model data — points, users, settings, messages — should be structs.

Equatable, Hashable, Comparable for free

Add protocol conformances and Swift writes the code:

struct Version: Equatable, Hashable, Comparable {
    var major: Int
    var minor: Int

    static func < (lhs: Version, rhs: Version) -> Bool {
        (lhs.major, lhs.minor) < (rhs.major, rhs.minor)    // tuples compare lexicographically
    }
}

print(Version(major: 1, minor: 2) < Version(major: 1, minor: 10))   // true
print(Set([Version(major: 1, minor: 0), Version(major: 1, minor: 0)]).count)   // 1

== and hash(into:) are synthesized when every stored property is itself Equatable/Hashable.

Common mistakes

  • Using a class by habit (from other languages) when a struct would do.
  • Forgetting mutating — the error is "cannot use mutating member on immutable value".
  • Assuming a class copy is independent. let b = a shares the object. To copy, write an explicit copy() method or use a struct.
  • Putting a struct into a let and expecting to modify properties.
  • Large structs with expensive copies in hot loops — usually fine thanks to copy-on-write for collections, but avoid giant inline structs passed by value endlessly.
  • Reference cycles between classes — see the ARC lesson.

Exercises

  1. Model a Rectangle struct with width, height, and methods for area and perimeter.
  2. Create a Money struct (cents: Int) with an init(dollars: Double); conform it to Comparable.
  3. Make class Account with a balance and deposit/withdraw. Create two variables referring to the same account and show that a deposit via one is visible via the other.
  4. Rewrite exercise 3 as a struct and observe what changes (hint: mutating, and copies).
  5. Write a failable initializer Temperature(kelvin:) that returns nil for negative values.
  6. Write a generic-free Queue struct with enqueue, dequeue, isEmpty, using two arrays.

Recap

  • struct = value type (copied); class = reference type (shared).
  • Memberwise initializers are free for structs; classes need explicit init.
  • Mutating struct methods are marked mutating.
  • === tests class identity; == tests value equality when Equatable.
  • Default to structs; use classes for shared identity, inheritance, or deinit.

Next: Properties & methods →