Why can't Swift initializers call convenience initializers on their superclass?

ClassInitializationSwift

Class Problem Overview


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?

Class Solutions


Solution 1 - Class

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:

Initializer Chaining

Solution 2 - Class

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 - Class

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 - Class

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 - Class

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.

Solution 6 - Class

Look at the WWDC-video "403 intermediate Swift" at 18:30 for an in depth explanation of initializers and their inheritance. As I understood it, consider the following:

class Dragon {
    var legs: Int
    var isFlying: Bool

    init(legs: Int, isFlying: Bool) {
        self.legs = legs
        self.isFlying = isFlying
    }

    convenience initWyvern() { 
        self.init(legs: 2, isFlying: true)
    }
}

But now consider a Wyrm-subclass: A Wyrm is a Dragon with no legs and no wings. So the Initializer for a Wyvern (2 legs, 2 wings) is wrong for it! That error can be avoided if the convenience Wyvern-Initializer simply can't be called but only the full designated Initializer:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}

Solution 7 - Class

Why don't you just have two initializers - one with a default value?

class A {
  var x: Int

  init(x: Int) {
    self.x = x
  }

  init() {
    self.x = 0
  }
}

class B: A {
  override init() {
    super.init()

    // Do something else
  }
}

let s = B()
s.x // 0

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionRobertView Question on Stackoverflow
Solution 1 - ClassCraig OtisView Answer on Stackoverflow
Solution 2 - ClassbebaView Answer on Stackoverflow
Solution 3 - ClassJulienView Answer on Stackoverflow
Solution 4 - ClassbigelerowView Answer on Stackoverflow
Solution 5 - ClassevanchinView Answer on Stackoverflow
Solution 6 - ClassRalfView Answer on Stackoverflow
Solution 7 - ClassJasonView Answer on Stackoverflow