Why can't Swift initializers call convenience initializers on their superclass?
Consider the two classes:
class A {
var x: Int
init(x: Int) {
self.x = x
}
convenience init() {
self.init(x: 0)
}
}
class B: A {
init() {
super.init() // Error: Must call a designated initializer of the superclass 'A'
}
}
I don't see why this isn't allowed. Ultimately, each class's designated initializer is called with any values they need, so why do I need to repeat myself in B
's init
by specifying a default value for x
again, when the convenience init
in A
will do just fine?
Solution 1:
This is Rule 1 of the "Initializer Chaining" rules as specified in the Swift Programming Guide, which reads:
Rule 1: Designated initializers must call a designated initializer from their immediate superclass.
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html
Emphasis mine. Designated initializers cannot call convenience initializers.
There is a diagram that goes along with the rules to demonstrate what initializer "directions" are allowed:
Solution 2:
Consider
class A
{
var a: Int
var b: Int
init (a: Int, b: Int) {
print("Entering A.init(a,b)")
self.a = a; self.b = b
}
convenience init(a: Int) {
print("Entering A.init(a)")
self.init(a: a, b: 0)
}
convenience init() {
print("Entering A.init()")
self.init(a:0)
}
}
class B : A
{
var c: Int
override init(a: Int, b: Int)
{
print("Entering B.init(a,b)")
self.c = 0; super.init(a: a, b: b)
}
}
var b = B()
Because all designated initializers of class A are overridden, class B will inherit all convenience initializers of A. So executing this will output
Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)
Now, if the designated initializer B.init(a:b:) would be allowed to call the base class convenience initializer A.init(a:), this would result in a recursive call to B.init(a:,b:).
Solution 3:
It's because you can end up with an infinite recursion. Consider:
class SuperClass {
init() {
}
convenience init(value: Int) {
// calls init() of the current class
// so init() for SubClass if the instance
// is a SubClass
self.init()
}
}
class SubClass : SuperClass {
override init() {
super.init(value: 10)
}
}
and look at:
let a = SubClass()
which will call SubClass.init()
which will call SuperClass.init(value:)
which will call SubClass.init()
.
The designated/convenience init rules are designed that a class initialisation will always be correct.
Solution 4:
I found a work around for this. It's not super pretty, but it solves the problem of not knowing a superclass's values or wanting to set default values.
All you have to do is create an instance of the superclass, using the convenience init
, right in the init
of the subclass. Then you call the designated init
of the super using the instance you just created.
class A {
var x: Int
init(x: Int) {
self.x = x
}
convenience init() {
self.init(x: 0)
}
}
class B: A {
init() {
// calls A's convenience init, gets instance of A with default x value
let intermediate = A()
super.init(x: intermediate.x)
}
}
Solution 5:
Consider extracting the initialization code from your convenient init()
to a new helper function foo()
, call foo(...)
to do the initialization in your sub-class.