11 · A struct holding a class
Copy a struct that holds a class, a class that holds a struct, an array of objects and a copy-on-write type — then count the instances.
Shape: what does this print · Reported: Zomato — how many instances after copying a struct that holds a class, and the reverse; Practo — explain copy-on-write · Verified: run with Swift 6.4
"Four small blocks. Tell me what prints, line by line — and at the end, how many objects exist."
final class Engine {
let name: String
var rpm = 0
init(_ name: String) {
self.name = name
print("init engine", name)
}
}
struct Car {
var engine: Engine
var speed = 0
}
let car1 = Car(engine: Engine("V8"))
var car2 = car1
car2.speed = 100
car2.engine.rpm = 3000
print(car1.speed, car1.engine.rpm, car1.engine === car2.engine) // ①
struct Address { var city: String }
final class Person {
var address: Address
init(_ address: Address) { self.address = address }
}
let p1 = Person(Address(city: "Berlin"))
let p2 = p1
var saved = p1.address
saved.city = "Paris"
p2.address.city = "Munich"
print(p1.address.city, saved.city) // ②
var garage = [Engine("A"), Engine("B")]
var copy = garage
copy[0].rpm = 900
copy.append(Engine("C"))
print(garage.count, copy.count, garage[0].rpm) // ③
final class Storage {
var items: [String]
init(_ items: [String]) { self.items = items }
}
struct Notes {
private var storage = Storage([])
var items: [String] { storage.items }
mutating func add(_ note: String) {
if !isKnownUniquelyReferenced(&storage) {
print("copying storage")
storage = Storage(storage.items)
}
storage.items.append(note)
}
}
var draft = Notes()
draft.add("a")
var shared = draft
shared.add("b")
shared.add("c")
print(draft.items, shared.items) // ④A hint, if you're stuck
- Copying a struct copies each of its fields. Ask what a field of class type actually holds.
leton a struct freezes its fields. Does it freeze the object a field points at?- Count the
init enginelines. Each one is exactly one new object — nothing else creates one. - In ④, ask how many variables point at the
Storageobject at the moment eachaddruns.
Answers (verified output)
init engine V8
0 3000 true
Munich Paris
init engine A
init engine B
init engine C
2 3 900
copying storage
["a"] ["a", "b", "c"]- ①
0 3000 true.var car2 = car1copies the struct:speedis copied, socar1.speedstays0. But theenginefield holds a reference — an address of an object — and copying an address gives you a second pointer to the same engine, not a second engine. So the rpm change shows throughcar1,===istrue, andinit engine V8printed once. Notecar1is alet: that stops you reassigningcar1.engine, not changing the engine's insides. - ②
Munich Paris. The reverse case.p2 = p1copies a reference — onePerson.saved = p1.addresscopies the struct value out of the object at that moment, sosavedis independent:Parisstays local.p2.address.city = "Munich"edits the one person's address in place, sop1sees it. - ③
2 3 900.copy = garagegives a second array value. Appending tocopydoesn't touchgarage(2 vs 3). But the elements are references, socopy[0]andgarage[0]are the sameEngine—900shows through. Writingcopy[0].rpmdidn't even copy the array: changing a property of a class element only reads the array to find the object. The two arrays still shared one buffer untilappend, and even then the copy holds references — no new engines. Only threeinit enginelines, for A, B and C. - ④ one
copying storage, then["a"] ["a", "b", "c"]. The firstaddruns whiledraftis the only owner of itsStorage, so it appends in place.shared = draftmakes two owners of oneStorage. The nextaddsees it's not uniquely referenced, copies, and only then writes. The thirdaddowns its new storage alone, so no second copy.draftnever seesborc— that's value semantics, built on a class, paid for only once. - How many objects? Six class instances: four
Engines (V8, A, B, C), onePerson, and twoStorages (the original and the one copy). Copying a struct, a class reference or an array never created one by itself.
The idea behind it
Swift has two kinds of types. A value type (struct, enum, tuple, Array, String) is like a photocopy: hand someone a copy and their scribbles don't reach yours. A reference type (class, actor) is like a shared Google Doc: hand someone the link and you both edit one document.
A struct is a bundle of fields, and copying it copies each field. If a field is an Int, you get a new number. If a field is a class, the field only holds the link, so you get a new link to the same document. That's why "a struct holding a class" behaves half like a value and half like a reference. The struct is copied; the object isn't.
Copy-on-write (CoW) is the trick that makes big values cheap. Instead of copying the whole photocopy every time you hand it over, both people share one copy until somebody wants to scribble — and only then does that person get their own. Swift checks "am I the only owner?" with isKnownUniquelyReferenced, which reads the object's reference count (how many variables point at it). Array, Dictionary and String do this internally. Your own struct only gets it if you write it, as Notes does.
How to make it unsurprising
- If the object is meant to be shared, say so in the type. A
Carwhose engine is shared between copies is a surprise. Either makeEnginea struct, or makeCara class so nobody expects copies to be independent. - If you need a class for storage, hide it behind CoW. Keep the class
private, and check uniqueness in every mutating method, asNotesdoes. Callers then get honest value semantics. - Don't expose mutable class properties from a struct.
car2.engine.rpm = 3000compiling on a copy is the bug factory; aletengine with novarinside it can't surprise anyone.
What I'm really scoring
That you count objects, not variables. Only an init makes an instance; copying a struct, a reference or an array never does. The weak answer says "structs are copied, so car1 is safe". The strong one says the struct is copied but the engine inside it is shared, explains why let doesn't freeze it, and can say how Array gets away with copying lazily — copy-on-write.
What I'd ask next
- "Does
var copy = garagecopy the array's memory right away?" — No. Arrays are copy-on-write: both variables share one buffer until one of them mutates the array itself. In ③ that's theappend—copy[0].rpm = 900doesn't count, because it changes the engine, not the array (checked by comparing the buffers' addresses before and after). The append copies the references, so still no new engines. - "Is
CarSendable?" — No. A struct is onlySendableif every stored property is.Engineis a class with avarand no locking, so it isn't, and neither isCar. CopyingCaracross threads would share the engine — a data race. - "Why can't
isKnownUniquelyReferencedtake alet?" — It takes its argumentinout, so the check and the write that follows happen on the same variable you own. Taking itinoutalso means the check doesn't add a temporary reference of its own, which would make the answer always "no". - "Does
===work on the structs?" — No.===asks "same object?", which only means something for class instances. For structs you compare contents with==(ifEquatable).