Struct vs Class
Value semantics versus reference semantics, and why Swift pushes you toward structs.
A struct is a *value*. When you assign it or pass it to a function, you get an independent copy. Changing the copy can't affect the original.
A class is a *reference*. When you assign it, both names point at the same object in memory. Changing it through one name changes it for everyone holding a reference.
Everything else — inheritance, deinit, reference counting, identity — follows from that one difference. Swift makes structs the default because most of the time, shared mutable state is a liability you didn't ask for.
let difference surprises people more than the copy does.struct PointValue { var x = 0 }final class PointRef { var x = 0 }
var a = PointValue(); var b = ab.x = 10// a.x == 0 — b is an independent copy
let c = PointRef(); let d = cd.x = 10// c.x == 10 — same object, and note `let` didn't stop the mutation
// `let` on a struct freezes its contents; on a class it only freezes the pointerlet frozen = PointValue()// frozen.x = 1 // error: frozen is a let constantlet stillMutable = PointRef()stillMutable.x = 1 // fine — the reference is constant, the object isn't
Show all 16 linesShow less
struct PointValue { var x = 0 }final class PointRef { var x = 0 }
var a = PointValue(); var b = ab.x = 10// a.x == 0 — b is an independent copy
let c = PointRef(); let d = cd.x = 10// c.x == 10 — same object, and note `let` didn't stop the mutation
// `let` on a struct freezes its contents; on a class it only freezes the pointerlet frozen = PointValue()// frozen.x = 1 // error: frozen is a let constantlet stillMutable = PointRef()stillMutable.x = 1 // fine — the reference is constant, the object isn'tTwo struct variables a and b each point at their own separate storage. Two class variables c and d both point at one shared object.
Assigning a struct copies the storage — two variables, two boxes.
- Assigning a struct copies the storage — two variables, two boxes.
- Assigning a class copies the pointer — two variables, one box.
Under the hood
Structs don't automatically live on the stack, and classes aren't automatically slow. The real distinctions are:
Allocation. A class instance is always a heap allocation with a reference count and a type header. A struct is stored inline wherever it lives — on the stack as a local, or inside its owner's memory as a property. A struct property inside a class instance is on the heap too.
Copy on write. Standard-library collections are structs, but copying an Array doesn't copy its elements. Both copies share one buffer, and the buffer is only duplicated when one of them is mutated *and* the reference count says it isn't uniquely held. That's why value semantics stay affordable at scale — and you can implement the same trick yourself with isKnownUniquelyReferenced.
Dispatch. A method on a final class or a struct can be called directly. A non-final class method goes through a vtable so subclasses can override it.
Quick check
Why does stillMutable.x = 1 compile when stillMutable is declared with let?
- `let` only applies to structs
- `let` makes the reference constant, not the object it points to; the object's own vars are still mutable
- Classes ignore `let` entirely
- It doesn't compile
Reveal answer
`let` makes the reference constant, not the object it points to; the object's own vars are still mutable
For a class, the constant is the pointer. You can't reassign stillMutable to a different instance, but you can mutate the instance it points at. For a struct, the value *is* the storage, so let freezes the whole thing including its properties. This is the single most common source of accidental shared mutation.
Interview question
How do you decide between a struct and a class for a new type?
Think first, then reveal
I start from struct and require a reason to move.
I move to a class when I need identity — when "is this the same object?" is a meaningful question distinct from "are these equal?". A user session, a network connection, a view controller: two of them with identical fields are still not interchangeable.
Also when I need shared mutable state that several owners must see change, when I need deinit to release something (an observer, a file handle, a socket), when I need inheritance or Objective-C interop, or when the object is genuinely long-lived and referenced from many places.
Everything else — models decoded from JSON, geometry, configuration, anything I want to pass across threads without worrying — is a struct. Value semantics mean I can hand it to another part of the app and know nobody can mutate it behind my back, which removes a whole category of bug rather than requiring me to defend against it.
I don't use size as a tiebreaker, because copy-on-write already handles large payloads.
What an interviewer listens for
- Defaults to struct and justifies the exception rather than the rule
- Names identity as the core distinction, not performance
- Mentions deinit, inheritance and ObjC interop as concrete class-only needs
- Knows copy-on-write removes the "too big to copy" argument
- Connects value semantics to thread safety
Key takeaway
Default to a struct. Reach for a class when you specifically need identity, shared mutation, inheritance, or deinit — and know that you're taking on reference counting and the possibility of cycles when you do.