Value and Reference Types
How struct value semantics differ from class reference semantics at runtime — copying, mutation, identity, copy-on-write, and where each type lives.
11 questions
JuniorTheoryVery commonWhat do == and === compare, and what must a type conform to for each?
What do == and === compare, and what must a type conform to for each?
== tests value equality and requires Equatable conformance. === (and !==) tests reference identity — whether two references point to the same instance — and works only on classes, with no conformance needed.
Common mistakes
- ✗Thinking
===works onstructvalues rather than only class references - ✗Believing
==is free withoutEquatableconformance - ✗Confusing value equality with reference identity
Follow-up questions
- →How does the compiler synthesize
Equatablefor astruct? - →Why can't
===be used to compare twostructvalues?
JuniorTheoryVery commonWhat actually differs at runtime between a struct and a class?
What actually differs at runtime between a struct and a class?
A struct is a value type — assigning or passing it copies the value, with no inheritance or identity. A class is a reference type — variables share one ARC-managed heap instance and gain inheritance, dynamic dispatch, and deinit.
Common mistakes
- ✗Believing a
structis heap-allocated behind a pointer like aclass - ✗Thinking assigning a
structshares the instance instead of copying it - ✗Assuming both types support inheritance and dynamic dispatch
Follow-up questions
- →Why does mutating a copied
structnever affect the original? - →When does a
classmethod still dispatch statically rather than dynamically?
JuniorCodeCommonPredict the output when the same program uses a class instead of a struct
Predict the output when the same program uses a class instead of a struct
It prints 10. With a class, var b = a copies only the reference, so a and b point to one instance; mutating b.x changes that object, so a.x reads 10. Reference types share the instance, not copy it.
Common mistakes
- ✗Expecting value-copy behaviour from a
classas with astruct - ✗Thinking
var b = adeep-copies the object rather than the reference - ✗Assuming class properties are shared statics across instances
Follow-up questions
- →How would you get an independent copy of the
classinstance? - →Why does
===now returntrueforaandb?
JuniorTheoryCommonWhy must a method changing a struct's stored property be mutating, and why can't you call it on a let?
Why must a method changing a struct's stored property be mutating, and why can't you call it on a let?
A value type's methods get an immutable self, so changing a stored property requires mutating, which makes self an inout. You can't call it on a let because a constant's value cannot change, so the compiler rejects it.
Common mistakes
- ✗Thinking any method can freely change a
struct's stored properties - ✗Believing the
letrestriction is about memory placement, not immutability - ✗Assuming a
classneedsmutatingthe same way astructdoes
Follow-up questions
- →Why does a
classmethod not needmutatingto change a property? - →How does
mutatingrelate toselfbeing passed asinout?
JuniorCodeCommonPredict the output when a copied struct variable is mutated
Predict the output when a copied struct variable is mutated
It prints 0. Assigning var b = a copies the value, so b is an independent copy; mutating b.x changes only that copy and leaves a unchanged. A struct is copied on assignment, not shared.
Common mistakes
- ✗Expecting the second variable to alias the first rather than copy it
- ✗Thinking a
structassignment shares storage like aclass - ✗Assuming the copy is lazy enough to leak later mutations back
Follow-up questions
- →How would the output change if
Pointwere aclass? - →At what moment is the value actually copied during
var b = a?
MiddleDebuggingCommonA struct captured in a closure shows the old value after the outer variable changed — explain and fix
A struct captured in a closure shows the old value after the outer variable changed — explain and fix
A capture list [value] copies the value type when the closure is created, a snapshot, so later mutations of the outer variable aren't seen. Drop the capture list to capture the variable itself, or use a class to share state.
Common mistakes
- ✗Thinking a capture list reads the variable live rather than snapshotting it
- ✗Blaming threading instead of value-copy capture semantics
- ✗Assuming
@escapingchanges what a capture list captures
Follow-up questions
- →How does capturing without a list differ from a
[counter]list? - →When is snapshotting with a capture list actually the behaviour you want?
SeniorDesignCommonYou are designing the model layer of an iOS app. Most models are structs for value semantics and safe copying, but for some types a struct is the wrong choice. Describe concretely when you would deliberately reach for a class instead of a struct — cover a shared mutable object that many parts of the app must observe as one thing, a type whose identity itself matters (two instances with equal fields must still count as distinct), lifecycle work that has to run in deinit, large payloads where copy-on-write buys nothing because you always mutate, and interoperability with an Objective-C or framework API that requires a reference type. For each case, explain what a struct cannot express and what the class actually buys you.
You are designing the model layer of an iOS app. Most models are structs for value semantics and safe copying, but for some types a struct is the wrong choice. Describe concretely when you would deliberately reach for a class instead of a struct — cover a shared mutable object that many parts of the app must observe as one thing, a type whose identity itself matters (two instances with equal fields must still count as distinct), lifecycle work that has to run in deinit, large payloads where copy-on-write buys nothing because you always mutate, and interoperability with an Objective-C or framework API that requires a reference type. For each case, explain what a struct cannot express and what the class actually buys you.
Reach for a class when many owners must share one mutable instance, when identity matters (===), when deinit runs cleanup, or when an Obj-C API needs a reference type. A struct copies, so it can't model one shared, identity-bearing object.
Common mistakes
- ✗Deciding solely on size rather than on identity and sharing needs
- ✗Thinking a
structcan model one shared, observable mutable instance - ✗Ignoring
deinitand Objective-C interop as forcing factors
Follow-up questions
- →Why does copy-on-write not remove the need for a
classwhen sharing mutation? - →How does reference identity (
===) change equality design for the type?
JuniorPerformanceOccasionalWhat does marking a class final change for the compiler, and what does it buy at runtime?
What does marking a class final change for the compiler, and what does it buy at runtime?
final forbids subclassing, so the compiler knows no override exists and can devirtualize — turning dynamic dispatch into direct, inlinable calls. At runtime this drops the vtable lookup and speeds up calls, especially across modules.
Common mistakes
- ✗Thinking
finalchanges allocation from heap to stack - ✗Believing dispatch stays dynamic even when the class is
final - ✗Assuming
finalis only about access control, not performance
Follow-up questions
- →When can whole-module optimization infer
finalon its own? - →Why does
finalhelp most across module boundaries?
JuniorTheoryOccasionalWhy does a struct with a stored property of its own type fail to compile, and how do you fix it?
Why does a struct with a stored property of its own type fail to compile, and how do you fix it?
A value type is stored inline, so a struct containing itself needs infinite size and won't compile. Add indirection through a reference — make it a class or use an indirect enum; a reference is pointer-sized.
Common mistakes
- ✗Thinking the error is about naming or initialization, not storage size
- ✗Believing a
structis stored behind a pointer like aclass - ✗Assuming
lazyor a default value resolves the infinite-size problem
Follow-up questions
- →Why is a reference pointer-sized regardless of what it points to?
- →When is an
indirect enuma better fix than aclassbox?
MiddleTheoryOccasionalHow does Array avoid copying on every assignment, and when exactly does the copy happen?
How does Array avoid copying on every assignment, and when exactly does the copy happen?
Assigning an Array copies only a reference to a shared buffer and bumps a retain count — O(1). The buffer is copied lazily at the first write to a still-shared buffer, when it is not uniquely referenced, keeping value semantics.
Common mistakes
- ✗Thinking assignment eagerly copies all elements
- ✗Believing the buffer is copied at assignment rather than at first write
- ✗Assuming two array variables share a buffer permanently like a
class
Follow-up questions
- →What does
isKnownUniquelyReferencedcheck before a mutation? - →Why is appending to a uniquely-referenced array not a copy?
SeniorCodeRareImplement copy-on-write by hand for a type backed by a class buffer using isKnownUniquelyReferenced
Implement copy-on-write by hand for a type backed by a class buffer using isKnownUniquelyReferenced
Wrap a heap class buffer in a struct. Before each mutation, check isKnownUniquelyReferenced(&buffer); if false, replace it with a fresh deep copy, then write. A copy happens only when a shared buffer is mutated — lazy value semantics.
Common mistakes
- ✗Copying the buffer on every assignment instead of only on shared mutation
- ✗Checking uniqueness in the getter rather than before a write
- ✗Thinking
isKnownUniquelyReferencedworks on value types orweakrefs
Follow-up questions
- →Why must the uniqueness check happen before the write, not after?
- →What breaks if two threads mutate the same shared buffer concurrently?