iosNudge

What are value semantics, and how does copy-on-write work?

MediumAsked oftenSwiftPerformance 3 min read

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 = a copies 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 copy

Build 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:

  1. Keep data in a private final class.
  2. Read through it freely.
  3. 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?

ActionCopies elements?
let b = aNo
Passing to a functionNo
Reading b[0]No
First mutation of a shared bufferYes, once
Later mutations of a unique bufferNo

Performance tips

  • Mutate in place. array.append(x) is better than array = array + [x].
  • Avoid keeping an extra reference while you mutate in a loop. It forces a full copy.
  • Pass with inout when 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 isKnownUniquelyReferenced thread-safe? It only reads the reference count. Your type must still not be mutated from two threads at once.
  • Does String use 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 let properties of value types). Then sharing is safe.