What are value semantics, and how does copy-on-write work?
TL;DR
Value semantics means every copy is independent. Changing one copy never changes another.
Copy-on-write (COW) is a trick to make that cheap. Copies share the same storage. The real copy happens only when one of them is changed.
Array, Dictionary, Set and String use COW. Your own structs do not get it for free.
Value semantics in one example
var a = [1, 2, 3]
var b = a // looks like a copy
b.append(4)
print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]a and b behave like separate values. That is value semantics.
Why COW is needed
An array can hold millions of elements. If every assignment copied all of them, Swift would be very slow.
So Array keeps its elements in a heap buffer. The struct only holds a reference to that buffer.
var b = acopies the reference. Both share one buffer. Cost: tiny.b.append(4)checks: "Am I the only owner of this buffer?"- If not, it copies the buffer first, then appends.
var a = Array(repeating: 0, count: 1_000_000)
var b = a // no element copy yet
b[0] = 1 // now the 1M elements are copied, once
b[1] = 2 // b owns its buffer now, no copyBuild your own COW type
Swift gives you isKnownUniquelyReferenced(_:). It returns true if only one strong reference points to the object.
final class Storage {
var items: [String]
init(items: [String]) { self.items = items }
}
struct Playlist {
private var storage = Storage(items: [])
var items: [String] { storage.items }
mutating func add(_ song: String) {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(items: storage.items) // copy before write
}
storage.items.append(song)
}
}
var p1 = Playlist()
p1.add("Intro")
var p2 = p1 // shares storage
p2.add("Outro") // p2 copies, then appends
print(p1.items) // ["Intro"]
print(p2.items) // ["Intro", "Outro"]Steps:
- Keep data in a private
final class. - Read through it freely.
- Before every write, check uniqueness. Copy if shared.
The trap: a class inside a struct
A struct does not have value semantics if it holds a mutable class and lets you change it.
final class Box { var value = 0 }
struct Wrapper {
let box = Box()
}
let w1 = Wrapper()
let w2 = w1
w2.box.value = 99
print(w1.box.value) // 99. Shared!The struct was copied, but the reference inside it points to the same Box. This is a common interview trick question.
When does a copy actually happen?
| Action | Copies elements? |
|---|---|
let b = a | No |
| Passing to a function | No |
Reading b[0] | No |
| First mutation of a shared buffer | Yes, once |
| Later mutations of a unique buffer | No |
Performance tips
- Mutate in place.
array.append(x)is better thanarray = array + [x]. - Avoid keeping an extra reference while you mutate in a loop. It forces a full copy.
- Pass with
inoutwhen a function must change a large value. It keeps the buffer unique.
func addDefaults(_ list: inout [String]) {
list.append("Default") // no copy if the caller's array is unique
}Why interviewers care
Value semantics make code easier to reason about. They remove a whole class of bugs where one screen changes data that another screen is still using. They are also easier to make Sendable for Swift concurrency.
Common follow-up questions
- Do custom structs get COW automatically? No. Only their stored properties that already use COW (like
Array) do. You must add it yourself for a custom buffer. - Is
isKnownUniquelyReferencedthread-safe? It only reads the reference count. Your type must still not be mutated from two threads at once. - Does
Stringuse COW? Yes. Small strings (up to 15 UTF-8 bytes on 64-bit) are even stored inline with no heap buffer. - Why must the storage class be
final? Not required, but it is a good practice. It avoids subclass surprises and allows faster dispatch. - Can value semantics apply to a class? Yes, if the class is immutable (only
letproperties of value types). Then sharing is safe.