How it works
The explanation, with examples you can run.
The difference in four lines
struct PointValue { var x = 0 }final class PointObject { var x = 0 } var v1 = PointValue(); var v2 = v1; v2.x = 5let o1 = PointObject(); let o2 = o1; o2.x = 5 print(v1.x, v2.x) // 0 5: two independent valuesprint(o1.x, o2.x) // 5 5: one shared objectprint(o1 === o2) // true: identity exists only for referencesNote let o1 and o2.x = 5: let on a class reference only fixes which object it points at. On a struct, let freezes every stored property, and methods that change a struct must be marked mutating.
Copy-on-write
Copying a 10,000-element array on every assignment would be slow, so the standard library collections and String use copy-on-write: the value holds a reference to a heap buffer, a copy shares that buffer, and the first mutation of a shared buffer makes a private copy.
var a = Array(repeating: 0, count: 10_000)var b = a // O(1): same buffer, reference count is now 2b[0] = 1 // the buffer is shared, so b copies it first, then writesprint(a[0], b[0]) // 0 1Your own structs don't get this automatically. A struct holding a class instance copies the reference, so two struct copies share one object. To give such a type value semantics, implement copy-on-write yourself: keep the class private, and before each mutation check isKnownUniquelyReferenced(&storage), copying the storage if it returns false.
Choosing
- Use a struct by default: models, configuration, view state, anything that is data. Equality means "same contents", which you get for free with
Equatable. - Use a class when identity matters (one shared cache, a connection, a view controller), when you need inheritance or
deinit, or when interoperating with Objective-C APIs that expect objects. - Use an actor when a reference type has mutable state that several tasks will touch (see the concurrency topic).
Costs to know
Values are not automatically on the stack: a struct inside an array, an existential, or a closure capture lives on the heap. Large structs are copied in pieces, and every String, Array or class inside one adds reference-counting traffic. In return the compiler can often skip copies (passing a value as a parameter is a borrow, not a copy) and has no shared mutable state to worry about, which is also what makes value types easy to send between threads in Swift 6.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.