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.