Copies, copy-on-write, and the cost of sharing
Swift’s central design decision is that most things are values. Int, String, Array,
Dictionary, every struct and every enum — assigning one gives the destination its own copy.
Classes are the exception: assigning one gives you a second name for the same object.
The consequence people quote is “structs are copied, classes are shared”. The consequence that matters is about reasoning: with a value, no other part of the program can change what you are holding. Nobody has a reference to reach through.
struct Point { var x: Int, y: Int }
var a = Point(x: 0, y: 0)
var b = a
b.x = 10
print(a.x) // 0 — `a` cannot be reached through `b`
Replace struct with class and a.x prints 10. Every function that received a earlier is now
holding something different than it was handed, and nothing in the type system marked that
possibility.
“Copying an array copies its elements” would make Swift unusable — passing a million-element array
to a function would be a million-element memcpy. It does not work that way. Array, String,
Dictionary and Set are value types wrapping a reference. The struct is a thin header holding a
pointer to a heap buffer. Copying the struct copies the pointer and bumps a reference count.
The buffer is only duplicated when someone writes to a copy — copy-on-write.
var first = [1, 2, 3]
var second = first // no allocation; both point at one buffer
second.append(4) // now the buffer is duplicated, because it was shared
You can implement the same trick, and for any type holding a large payload you should:
struct Pixels {
private final class Buffer {
var data: [UInt8]
init(data: [UInt8]) { self.data = data }
}
private var buffer: Buffer
init(data: [UInt8]) { buffer = Buffer(data: data) }
var data: [UInt8] { buffer.data }
mutating func setPixel(at index: Int, to value: UInt8) {
if !isKnownUniquelyReferenced(&buffer) {
buffer = Buffer(data: buffer.data)
}
buffer.data[index] = value
}
}
isKnownUniquelyReferenced is the whole mechanism. If this value holds the only reference to the
buffer, mutate it in place. If anyone else is looking, duplicate first. Note the buffer class is
final and private: uniqueness is only meaningful if no subclass and no outside code can hold a
reference you did not account for.
Warning
isKnownUniquelyReferenced returns false for Objective-C objects, always. A copy-on-write
wrapper around an imported NSObject subclass will copy on every single write.
A mutating method on a struct does not modify the value in place the way a class method does. It
takes the value as inout, and inout means “copy in, copy back out” — semantically. The optimiser
usually turns that into an in-place update, but the semantics are what the language guarantees, and
they explain otherwise-baffling rules:
struct Counter {
var count = 0
mutating func increment() { count += 1 }
}
let fixed = Counter()
// fixed.increment() // error: `fixed` is a `let`
The call is rejected not because the method touches memory, but because calling it means assigning a
new Counter back to fixed.
This is also why mutating methods are unavailable through a protocol existential held in a let,
and why a mutating method cannot escape self into a closure.
Reach for a struct by default. Reach for a class when you have one of these:
User values with identical fields are the same user; two
URLSession objects are not interchangeable no matter how alike they look.The mistake I see most often is the reverse of what the style guides warn about. It is not
“overusing classes”. It is a struct holding a class reference and quietly acquiring reference
semantics through the back door:
final class Session { var token: String? }
struct Client { // looks like a value
let session: Session // is not one
}
Copy a Client and you have two clients sharing a token. The type says value; the behaviour says
reference. If that is what you want, make it a class so readers can see it.