Lesson 11 / 24
Structures and Classes
Measuring copying on assignment for structures, sharing for classes, when a default value is built, and why a copy appears at the first mutation.
Published Updated
In this lesson
- Measures that assigning a structure gives an independent copy and assigning a class gives a shared object
- Shows that a default value is evaluated per instance and only when none is given
- Measures that with copy-on-write the copy appears at the first mutation, not at assignment
- Shows that a single-owner value, or one shared inside a class, is never copied
Contents
The previous lesson measured the ways of opening a box and listed when each one evaluates its expression. The value that comes out may be an Int or a String, but the real data of a program is usually a type that keeps several fields together: a point, an order, a document. Swift has two basic forms for such types (enumerations and actors are the subject of other lessons). A structure is a value type: its content is copied when it is assigned. A class is a reference type: assigning it gives a second name to the same object.
Most books explain this difference with the question “who sees what”: does one name see the change made through the other? This lesson asks a different question: when does the copy appear? Does the assignment itself produce a copy, or does the copy appear only at the first mutation? The course’s measuring axis splits this question into four moments: immediately, on first use, on every use, never. The Probe class is given below exactly as it is.
The Core
Every lesson uses the same core. Probe counts how many times an expression is evaluated (eval) and how many times it is used (use); the moment property derives the moment from those two numbers. In the binding measurement of the opening lesson, the distribution over eight expressions was 2/1/3/2: two immediately, one on first use, three on every use, two never. In this lesson we measure only our own slice.
// Probe.swift — records when an expression is evaluated
final class Probe {
let label: String
private(set) var evaluations = 0
private(set) var uses = 0
private var evaluatedBeforeUse = false
init(_ label: String) {
self.label = label
}
func eval<T>(_ value: T) -> T {
evaluations += 1
if uses == 0 { evaluatedBeforeUse = true }
return value
}
func use<T>(_ access: () -> T) -> T {
uses += 1
return access()
}
var moment: String {
if evaluations == 0 { return "never" }
if evaluations > 1 { return "on-every-use" }
return evaluatedBeforeUse ? "immediately" : "on-first-use"
}
func report() {
print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
}
}
func tally(_ probes: [Probe]) {
for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
let count = probes.filter { $0.moment == moment }.count
print("\(moment): \(count)")
}
}
The first measurements below use three types. Point is a structure that holds two integers, and Counter is a class that holds a single counter. Order is a structure with a field that has a default value; the default value is a function call, and when the function runs it prints a line. This block has no output, because it only defines the types.
// Types.swift — a structure, a class and a structure with a default value
struct Point {
var x: Int
var y: Int
}
final class Counter {
var value = 0
}
func makeNote() -> String {
print("default value built")
return "none"
}
struct Order {
var id: Int
var note = makeNote()
}
Assignment: Copy or Share
We write the same two lines with a structure and with a class. With the structure, after var second = first and second.x = 10, first does not change. With the class, after let right = left and right.value = 5, the two names are bound to the same object and both see 5.
// main.swift — a structure is copied on assignment, a class is shared let first = Point(x: 1, y: 2) var second = first second.x = 10 print(first.x, second.x) let left = Counter() let right = left right.value = 5 print(left.value, right.value)
1 10 5 5
The first line shows the structure: first.x stayed 1 and second.x became 10. The assignment gave an independent copy. The second line shows the class: left.value and right.value are both 5, because there is a single Counter object and two names are bound to it. Notice that right is a let constant and the value inside the object could still change: let forbids binding the name to another object, not changing the content of the bound object. With a structure, let means the whole value is immutable: a field of an immutable structure cannot be assigned, even if the field is declared with var.
The output separates the two types but does not say when the copy appears. The sentence “a structure is copied on assignment” exists in textbooks as a model; what we measure is observable behavior: after the change, the two values are independent. Whether the copy physically appears at assignment or later is measured below.
TS9. This measurement is for a structure with two integer fields and a class with a single field. Structures whose fields hold a reference to another class, and the copying of class fields inside a structure, are not part of this measurement.
Passing to a Function
Assignment is only one face of the value and reference distinction; the same distinction shows up when an argument is passed to a function. When a function takes a structure as a parameter, it works with its own copy: a change made inside does not reach the caller’s value. With a class parameter, the function and the caller look at the same object. In the measurement below, moved changes and returns a copy of the structure, while bump changes the counter in place.
// main.swift — a structure passed to a function is copied, a class is shared
func moved(_ point: Point) -> Point {
var copy = point
copy.x += 100
return copy
}
func bump(_ counter: Counter) {
counter.value += 100
}
let origin = Point(x: 1, y: 2)
let shifted = moved(origin)
print(origin.x, shifted.x)
let ticker = Counter()
bump(ticker)
print(ticker.value)
1 101 100
On the first line origin.x stayed 1 and shifted.x became 101: copy inside moved was a separate value and the change was written only to it. On the second line ticker.value became 100, because bump changed the same Counter object, and the function returns nothing. So the signature of a function whose parameter is a structure tells the reader “this function does not change the value I call it with”, as long as the structure holds only values and the parameter is not inout. The signature of a function with a class parameter gives no such guarantee; you have to look at the body.
The practical result of this distinction shows in debugging. If a structure’s value changed unexpectedly, the change was made in the scope of that value itself; if a class object changed unexpectedly, every place that can reach the object is a suspect. Using value types between function signatures narrows the field of suspects.
TS10. This measurement is for one structure parameter and one class parameter. Passing with an inout parameter, and a structure captured by a closure, are not part of this measurement.
The Default Value: Once per Instance
The note field of the Order structure starts with the result of the makeNote() function. This default value is not built once and shared among all instances; every Order instance builds its own default value. Moreover, the value is not computed at all if the caller supplies the field itself. We measure with three orders: two with the default note and one with an explicit note.
// main.swift — the default is evaluated per instance, but only when not given let o1 = Order(id: 1) let o2 = Order(id: 2) let o3 = Order(id: 3, note: "urgent") print(o1.note, o2.note, o3.note)
default value built default value built none none urgent
makeNote() ran twice, for two of the three orders. For the third order, which gave the note explicitly, the function was never called. Both places where it ran appear before the print line; that is, the default value was computed on the calling line, while the instance was being built.
One result follows: every expression written as a default value is evaluated as many times as there are instances, minus the number of supplied values. If an expensive expression is written as a default value, that cost is paid on every instance. The Properties lesson measures deferring this cost with lazy.
TS11. This measurement was made with three instances and one field with a default value. The evaluation order of several fields with default values, and the same case in classes, were not measured separately.
Copy-on-Write
When a large value type is assigned, copying all of it for real would be wasteful. Some types of the standard library therefore use a technique called copy-on-write: assignment only adds a second reference to a storage, and the copy appears only when the shared storage has to be changed, at the first mutation. We can build the same technique by hand in our own type. The Document structure keeps its text in a Storage class. append first asks with isKnownUniquelyReferenced whether we are the only owner of the storage; if not, it copies the storage and reports the moment of copying through the onCopy closure.
// Document.swift — a structure boxed in a class, copying on write
final class Storage {
var text: String
init(text: String) {
self.text = text
}
}
struct Document {
private var storage: Storage
private let onCopy: () -> Void
init(text: String, onCopy: @escaping () -> Void) {
storage = Storage(text: text)
self.onCopy = onCopy
}
var text: String {
storage.text
}
mutating func append(_ more: String) {
if !isKnownUniquelyReferenced(&storage) {
onCopy()
storage = Storage(text: storage.text)
}
storage.text += more
}
}
final class Holder {
var document: Document
init(_ document: Document) {
self.document = document
}
}
Look at the code closely. The storage field is private, so it is invisible from outside; only the read-only text property is exposed. Changes pass only through append, which is marked mutating, and the decision to copy is made only there. This narrowing matters: because the copying rule stays in one place, no other method of the type can change the storage while it is shared. The onCopy closure is there for the measurement; production code would not contain such a notification, but here it lets us count in exactly which call the copy appears. The Holder class will be used in the last measurement: it makes a Document value the field of a class object.
This block has no output; it only defines Document and the Holder that wraps it in a class. Now we make the assignment duplicate = original, change both, and measure exactly when the copy appears. The onCopy closure is wrapped in cow.eval; append is called twice.
// main.swift — the copy appears at the first mutation, not at assignment
let cow = Probe("copy-on-write")
let original = Document(text: "ab", onCopy: { _ = cow.eval(()) })
var duplicate = original
_ = cow.use { duplicate.append("c") }
_ = cow.use { duplicate.append("d") }
cow.report()
print(original.text, duplicate.text)
copy-on-write: 1 evaluations, 2 uses -> on-first-use ab abcd
The assignment itself produced no copy: the evaluation count is one and the moment is on first use. The first append was sharing the storage with another value, so the copy appeared at that moment. The second append was the sole owner of the copied storage, so no copy was needed. The last line confirms the result: original is still ab, while duplicate is abcd. There are two values, but only one copy took place, and it took place not at assignment but at the first mutation.
We also write out what the numbers mean. One evaluation says the copy appeared once; two uses say two append calls were made. If the evaluation count were equal to the use count, every change would give birth to a copy and the moment would be on every use. If it were zero, no change would have needed a copy and the moment would be never. The measured one is below two and it appeared at the first use; these three numbers together decide the moment.
This measurement answers the question we asked at the start. In a value type like Document, which shares its storage, the copy appears after assignment, during the first write. For the reading side there is no difference; the cost of the copy falls on the first writer. The choice between a value type and a reference type can therefore be read not only as a semantic preference but also as a question of when the cost is paid.
TS12. This measurement is only for the Document type we built by hand; the copy-on-write behavior of the standard library’s array, dictionary and string types was not measured in this lesson.
The Bounding Measurement
The rule we built for copy-on-write is this: the copy appears at the first mutation. The rule has a condition, the storage must be shared with another owner. This condition does not hold in two cases. In the first, nobody has become a co-owner of the solo document; in the second, the document is inside a class and two names are bound to the same class object, that is, there is no second Document value sharing the storage. In both, append is called twice.
// main.swift — a single-owner document, or one shared inside a class, is not copied
let unique = Probe("single-owner")
var solo = Document(text: "ab", onCopy: { _ = unique.eval(()) })
_ = unique.use { solo.append("c") }
_ = unique.use { solo.append("d") }
let shared = Probe("class-sharing")
let holder = Holder(Document(text: "ab", onCopy: { _ = shared.eval(()) }))
let alias = holder
_ = shared.use { holder.document.append("c") }
_ = shared.use { alias.document.append("d") }
unique.report()
shared.report()
print(solo.text, holder.document.text, alias.document.text)
single-owner: 0 evaluations, 2 uses -> never class-sharing: 0 evaluations, 2 uses -> never abcd abcd abcd
In both cases onCopy was never called: the moment is never. The single-owner document was not copied even at the first mutation, because there was no second owner to share with. The document inside the class was not copied either: since holder and alias are bound to the same Holder object, an append made through one name is visible through the other, and a second copy of the Document value never appears. The last line confirms this sharing: all three names print abcd.
This limit narrows the lesson’s rule. The sentence “a value type is copied on assignment” and the sentence that completes it, “the copy appears at the first mutation”, hold only if there are two separate values sharing the storage. If a value type is the field of a class, the reference semantics of the class wins: no copy appears, because the class object is already single.
TS13. This measurement is for a single class object and two names. The case where the class object itself, together with its Document field, is assigned to another value, or where two different Holder objects carry the same document, was not measured.
Summary
- A structure gives an independent copy on assignment; when a class is assigned, two names are bound to the same object.
- A class name declared with
letdoes not make the object immutable; a structure declared withletmakes the whole value immutable. - A default value is evaluated once per instance, and never if the caller supplies the field.
- With copy-on-write the copy appears not at assignment but at the first mutation, and only if the storage is shared.
- With a single-owner value, or one shared inside a class, the copy never appears.
- This lesson’s moment: on first use. In a copy-on-write document the copy appears at the first mutation.
Next Step
In this lesson we measured whether a type is a value or a reference and when the copy appears. Some types are not a list of fields but a choice among several distinct states: a measurement is either in degrees or a label. The next lesson measures enumerations, which describe those states and the values carried along with each of them.
To keep your progress and take notes, Log in
My notes
Log in to take notes.