13 · Initializer order
Convenience to designated, two-phase init, the didSet that doesn't fire, an overridden method called from super.init, required and failable inits.
Shape: what does this print · Reported: PhonePe — walk through the flow of initialisation from convenience to designated · Verified: run with Swift 6.4
"Walk me through the order. Every
didSetfire?"
func trace(_ label: String) -> Int {
print("default value:", label)
return 0
}
class Vehicle {
var odometer = trace("odometer")
var wheels: Int {
didSet { print("didSet wheels =", wheels) }
}
init(wheels: Int) {
print("Vehicle.init starts")
self.wheels = wheels
self.wheels += 1
describe()
print("Vehicle.init ends")
}
convenience init() {
print("Vehicle.convenience starts")
self.init(wheels: 4)
self.wheels = 10
print("Vehicle.convenience ends")
}
func describe() { print("Vehicle.describe") }
}
print("① ---")
_ = Vehicle()
class Bike: Vehicle {
let brand: String
init(brand: String) {
self.brand = brand
print("Bike phase 1 done")
super.init(wheels: 2)
print("Bike phase 2 starts")
wheels = 3
}
override func describe() { print("Bike.describe brand =", brand) }
}
print("② ---")
_ = Bike(brand: "Brompton")
class Shape {
let sides: Int
required init(sides: Int) { self.sides = sides }
init?(name: String) {
guard name == "triangle" else { return nil }
self.sides = 3
}
}
final class Square: Shape {
required init(sides: Int) {
print("Square.init sides =", sides)
super.init(sides: sides)
}
}
func make<S: Shape>(_ type: S.Type) -> S { type.init(sides: 4) }
print("③ ---")
let square = make(Square.self)
print(type(of: square), square.sides)
print("④ ---")
print(Shape(name: "triangle")?.sides as Any, Shape(name: "circle")?.sides as Any)And one more: does this compile?
class Bike: Vehicle {
let brand: String
init(brand: String) {
describe() // ⑤
self.brand = brand
super.init(wheels: 2)
}
}A hint, if you're stuck
- A convenience init can't set anything up itself — it must hand off to a designated init of the same class. When do the default values like
odometerget assigned? - Observers don't fire for some assignments made inside initializers. Which ones?
- In ②,
describe()is called from insidesuper.init. Whichdescriberuns, and hasbrandbeen set yet?
Answers (verified output)
① ---
Vehicle.convenience starts
default value: odometer
Vehicle.init starts
Vehicle.describe
Vehicle.init ends
Vehicle.convenience ends
② ---
Bike phase 1 done
default value: odometer
Vehicle.init starts
Bike.describe brand = Brompton
Vehicle.init ends
Bike phase 2 starts
didSet wheels = 3
③ ---
Square.init sides = 4
Square 4
④ ---
Optional(3) nilAnd ⑤ doesn't compile:
p13err.swift:11:9: error: 'self' used in method call 'describe' before 'super.init' call- ① convenience first, then across to designated. The convenience body runs until
self.init(wheels:). Only then does the designated init run — and it starts by assigning the default values (default value: odometer) before its own body. NodidSetline at all: neitherself.wheels = wheelsnor+= 1inVehicle's own designated init fires it, and — we checked — neither doesself.wheels = 10inVehicle's own convenience init, even though it runs after the object is fully built. - ② subclass first, then up, then back down.
Bikesetsbrand(its phase 1), then callssuper.init, which setsVehicle's properties. Inside it,describe()dispatches toBike's override — the object is aBikethe whole time — andbrandis alreadyBrompton, because Swift forces a subclass to set its own properties before callingsuper.init. Aftersuper.initreturns, phase 2:wheels = 3in the subclass init does fire the superclass'sdidSet. - ③
requiredmakes an init part of the contract for every subclass.makecallstype.init(sides:)on a metatype it only knows is someShape. That's only legal because the init isrequired, so every subclass must provide it — andSquaremust repeatrequired. - ④ a failable init returns an optional.
init?can give up withreturn nil. The triangle builds, the circle doesn't. - ⑤ won't compile.
describe()usesself, andselfisn't fully built until phase 1 is done all the way up the chain. Swift refuses to let you touch it early.
The idea behind it
Building a class instance is two-phase initialisation. Think of building a house floor by floor. Phase 1 goes up: each class, starting with the most specific, fills in the stored properties it declared, then hands over to its parent with super.init. When the top class has filled in its own properties, every property of the object has a value. Phase 2 comes back down: each initializer, now able to use self freely, can call methods and change properties.
A designated initializer is a main one: it fills in its own class's properties and calls up to the parent. A convenience initializer is a shortcut: it must call another initializer of the same class first, and only then tweak things. Delegation goes "across" for convenience inits and "up" for designated ones.
Property observers (willSet/didSet) exist to react to changes. Assignments made inside the declaring class's own initializers — designated or convenience, even after self.init — never fire its observers. A subclass setting a parent's property after super.init is a change the parent should hear about — so they fire.
Swift's phase-1 rule is why ② is safe. In Objective-C, a parent's init that calls an overridden method can reach a subclass whose properties are still nil or zero. Swift makes that impossible.
How to make it unsurprising
- Don't call overridable methods from an initializer. ② works in Swift, but the reader has to know phase 1 to trust it. Make
describefinal, or call it after construction. - Don't put work you rely on in a
didSetfor a value set ininit. It won't run. If the side effect matters on first set, call it explicitly from the init too. - Prefer one designated init plus convenience inits. Every path then funnels through one place that sets every property, which keeps the order easy to follow.
What I'm really scoring
That you can explain the order from the two-phase rule, not recite it. Up the chain setting properties, then back down using self. The strong answer also gets the two traps: the didSet that stays silent inside the class's own inits, and why calling an overridden method from super.init is safe in Swift but not in Objective-C. ⑤ should be "no, because self isn't built yet" — not a guess.
What I'd ask next
- "When does a subclass inherit its parent's designated inits?" — When it defines no designated init of its own (and its new properties all have default values). If it implements all of the parent's designated inits, it also inherits the parent's convenience inits.
- "Why must
Squarewriterequiredagain?" — So every subclass ofSquareis also forced to have it; the requirement keeps passing down the chain. Afinalclass can't have subclasses, but the keyword is still required. - "Structs don't have any of this — why?" — No inheritance, so no phase across classes. A struct gets a memberwise init for free, and its observers likewise don't fire for assignments in its own init.
- "
init?vsinit() throws?" — Usethrowswhen the caller needs to know why it failed;init?when "it didn't work" is the whole story, likeInt("abc").