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:
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 (
NSObjectsubclasses). - A
deinitto 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 = ashares the object. To copy, write an explicitcopy()method or use a struct. - Putting a
structinto aletand 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¶
- Model a
Rectanglestruct withwidth,height, and methods for area and perimeter. - Create a
Moneystruct (cents: Int) with aninit(dollars: Double); conform it toComparable. - Make
class Accountwith a balance anddeposit/withdraw. Create two variables referring to the same account and show that a deposit via one is visible via the other. - Rewrite exercise 3 as a struct and observe what changes (hint:
mutating, and copies). - Write a failable initializer
Temperature(kelvin:)that returnsnilfor negative values. - Write a generic-free
Queuestruct withenqueue,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 whenEquatable.- Default to structs; use classes for shared identity, inheritance, or
deinit.
Next: Properties & methods →