Lesson 13 / 24
Properties
Measuring when stored, computed and observed properties, lazy properties and static let values are set up.
Published Updated
In this lesson
- Tells apart when a stored, a computed and an observed property set up their value
- Measures that a lazy property is evaluated once, on the first read
- Reads the difference between a default value and lazy from the output visible while instances are created
- Shows that a lazy property in a structure multiplies along with the copy
Contents
The previous lesson showed that the associated value of an enumeration is evaluated once, when the case is constructed, and that its computed property is evaluated on every read. The same distinction is not special to enumerations. The properties of structures, classes and enumerations name the data of a type, and each property sets up its value at its own time: some while the instance is created, some on every read, some on the first read.
This lesson measures four kinds of property. A stored property keeps a value inside the instance. A computed property stores no value and runs an expression when read. An observed property is a stored property that runs its willSet and didSet property observers before and after it changes. A lazy property does not set up its value until the first read. The course’s measuring axis is the same in this lesson: when is this expression evaluated? There are 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)")
}
}
Three Basic Kinds
The first measurement puts stored, computed and observed properties side by side. In the Rectangle structure width and height are stored properties, and area is a computed one: it holds no value and does the multiplication on every read. target in the Thermostat class is a stored property that carries willSet and didSet observers. willSet sees the upcoming value under the name newValue before the value is written, and didSet sees the old value under the name oldValue after it is written.
// main.swift — stored, computed and observed property
struct Rectangle {
var width: Int
var height: Int
var area: Int {
width * height
}
}
final class Thermostat {
var target = 20 {
willSet { print("willSet", target, "->", newValue) }
didSet { print("didSet", oldValue, "->", target) }
}
init(target: Int) {
self.target = target
}
}
let rect = Rectangle(width: 3, height: 4)
print(rect.area)
let thermostat = Thermostat(target: 18)
print("initializer done")
thermostat.target = 22
thermostat.target = 25
12 initializer done willSet 18 -> 22 didSet 18 -> 22 willSet 22 -> 25 didSet 22 -> 25
The first line gives the 3 * 4 result of the area property. Before the second line, the initializer had written 18 into the target field, but the observers did not run: no line appeared until the initializer finished. Observers run only on assignments made after the instance is fully set up; an assignment in the type’s own initializer does not trigger them. The next two assignments ran the observers, and for each assignment the order was willSet, the assignment, didSet. In the willSet output the right-hand value shows the upcoming value, and in the didSet output the left-hand value shows the old value; in the second, since target is already updated, the old value could not have been reached without the name oldValue.
The three kinds answer three different questions. A stored property answers “where does the value sit?”. A computed property answers “how is the value derived from other values?”. An observed property answers “what should happen when the value changes?”. None of them is lazy: the stored value is set up at construction, the computed value on every read, and the observer on every write.
TS18. This measurement is for a computed property in a structure and an observed property in a class. Observers in structures, the direction of change of a computed property, and the behavior of observers on a property passed with inout were not measured separately.
Computed Properties and set
A computed property is not only read, it can be written too. For this, a set is written next to the get; set takes the assigned value under the name newValue and is obliged to reflect it into the stored properties. In the Temperature structure, fahrenheit neither stores a value nor is a source itself. When read it is derived from celsius, and when written it updates celsius.
// main.swift — a computed property tied to a stored field with get and set
struct Temperature {
var celsius: Double
var fahrenheit: Double {
get { celsius * 9 / 5 + 32 }
set { celsius = (newValue - 32) * 5 / 9 }
}
}
var reading = Temperature(celsius: 100)
print(reading.fahrenheit)
reading.fahrenheit = 32
print(reading.celsius)
212.0 0.0
With celsius at 100, reading fahrenheit gave 212.0. Then 32 was written to fahrenheit and the set block pulled celsius to 0.0. A single piece of data can be read and written in two different representations, and the real value sits in one place, the celsius field. This is the strength of a computed property: it offers two forms of the same information without causing inconsistency.
The same result follows from the enumeration example in the previous lesson: a computed property runs again on every read. The difference here is that with set it also works in the opposite direction.
Lazy Properties
Some properties are expensive and not needed in every instance. Computing them while the instance is created is wasted work for an instance that will never be read. The lazy keyword leaves the property unset until the first read. The first read evaluates the expression once and stores the result inside the property, and later reads give the stored value. The summary property in the Report class computes the total of the rows lazily.
// main.swift — a lazy property is evaluated once, on the first read
final class Report {
let probe: Probe
var rows = [1, 2, 3]
lazy var summary: Int = probe.eval(rows.reduce(0, +))
init(probe: Probe) {
self.probe = probe
}
}
let summaryProbe = Probe("lazy-first-read")
let report = Report(probe: summaryProbe)
_ = summaryProbe.use { report.summary }
_ = summaryProbe.use { report.summary }
summaryProbe.report()
lazy-first-read: 1 evaluations, 2 uses -> on-first-use
One evaluation, two reads, moment on first use. The computation was done at the first read and the second read gave the stored result. This is what separates it from a computed property: a computed property would have made two evaluations in two reads. lazy is not a computed property, it is a deferred stored property. That is why lazy has to be a var; because the first read writes the value into the property, a writable stored property is needed.
The Difference Between a Default Value and lazy
In the previous lesson we measured that a default value is evaluated once per instance. Now we write the same expression in two forms so the difference becomes visible. The Ticket class carries two properties: eagerId calls the same makeId function through a default value, and lazyId calls it through lazy, and the function prints a line every time it is called. Two tickets are created, and then only the first ticket’s lazyId property is read twice.
// main.swift — a default value runs at creation, lazy at the first read
func makeId(_ kind: String) -> Int {
print("built", kind)
return 7
}
final class Ticket {
var eagerId = makeId("eager")
lazy var lazyId = makeId("lazy")
}
let tickets = [Ticket(), Ticket()]
print("created")
print(tickets[0].lazyId)
print(tickets[0].lazyId)
built eager built eager created built lazy 7 7
The first two lines show that the eagerId property of both tickets was set up before created was printed: each instance computed its own default value at creation time. For lazyId there are no such lines. The lazy line appeared only when tickets[0].lazyId was first read, and it appeared only once; the second read gave 7 from the stored value. The lazyId property of the second ticket was never read, so it was never set up.
The difference is summarized briefly. The expression of a default value runs for every instance, while the instance is being created. A lazy expression runs only for the instances that are read, at the first read. If twenty tickets are created and only one’s id is looked at, lazy does one computation instead of twenty.
static let
Values that belong to the type, that is static properties, are bound not to instances but to the type itself. A value defined with static let is lazy: it is set up on first access, and set up once. Config.version is built with a closure, and the closure prints a line each time it runs.
// main.swift — static let is set up on first access
enum Config {
static let version: String = {
print("static let built")
return "1.0"
}()
}
print("before")
print(Config.version)
print(Config.version)
before static let built 1.0 1.0
The before line was printed before the first access; the static let built line appeared only when Config.version was first read, and it appeared only once. The later read gave the value 1.0 from the stored copy. This is the same behavior as lazy, but there is one difference: static let is not tied to an instance, and unlike lazy it does not have to be a var, so it can be used as an immutable lazy constant.
TS19. This measurement is for a static let set up with a closure. static var and static computed properties were not measured separately.
The Bounding Measurement
The rule we built for the lazy property is this: the expression is evaluated once, on the first read. This rule holds for a class instance, because a class instance is a single shared object. We move the same property into a structure. Structures are copied on assignment, and a copy carries its own copy of a lazy property that has not been read yet. In the Sheet structure, total is a lazy property; sheetA is copied and both copies are read once.
// main.swift — a lazy property in a structure multiplies with the copy
struct Sheet {
let probe: Probe
var rows = [1, 2, 3]
lazy var total: Int = probe.eval(rows.reduce(0, +))
}
let sheetProbe = Probe("struct-lazy-copy")
var sheetA = Sheet(probe: sheetProbe)
var sheetB = sheetA
_ = sheetProbe.use { sheetA.total }
_ = sheetProbe.use { sheetB.total }
sheetProbe.report()
struct-lazy-copy: 2 evaluations, 2 uses -> on-every-use
Two reads, two evaluations, moment on every use. Both copies set up the same lazy property separately, because the property had not been set up yet at the moment of copying, and each copy received its own unset property. The same value was computed twice.
This result narrows the lesson’s rule. The word “once” holds for one instance. In a structure there are as many instances as copies, and a lazy property that has not yet been read is set up independently in each copy at its first read. In a class this situation does not arise, because no copy appears.
A lazy property in a structure has a second limit, and it too cannot be measured, because it does not compile. Reading writes to the property, so it requires the structure to be mutable. The fragment below reads the lazy property of a structure defined with let and does not compile: the compiler rejects it. The error message said that a mutating read cannot be used on a structure defined with let. The fragment stands here only for explanation.
struct Sheet2 {
var rows = [1, 2, 3]
lazy var total: Int = rows.reduce(0, +)
}
let fixed = Sheet2()
print(fixed.total) // does not compile: a lazy property cannot be read on a let structure
This is a side consequence of the lazy property: the read includes a write. A structure instance meant to be read has to be a var. A class does not have the same restriction, because a class name defined with let fixes only the name, not the content of the object.
TS20. This measurement is for two copies and a single lazy property. Nested structures, cases where a lazy property depends on another lazy property, and, in the rejected fragment, only a let structure were tried; other ways of fixing a value were not tried.
Summary
- A stored property is set up at construction, a computed property on every read, and an observer on every write; an assignment in the type’s own initializer does not trigger observers.
- A computed property can be written with
setand reflects the real value into another stored property. - A
lazyproperty is evaluated once, on the first read; a default value runs for every instance during creation. static letis set up on first access, and set up once.- A not-yet-read
lazyproperty in a structure multiplies with the copy; it cannot be read on aletstructure. - This lesson’s moment: on first use. A
lazyproperty is evaluated once, on the first read.
Next Step
In this lesson we measured when properties set up their value. Types do not only carry data, they can also promise behavior. The next lesson measures protocols: the contracts that say which properties and methods a type must provide, and the moment at which a property is evaluated when it is called from code that uses the contract.
To keep your progress and take notes, Log in
My notes
Log in to take notes.