为什么 Swift 初始化器不能在它们的父类上调用方便初始化器?

考虑一下这两个类:

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'
}
}

我不明白为什么不能这么做。最终,每个类的指定的初始化器被调用任何值,所以为什么我需要在 Binit中重复自己,再次为 x指定一个默认值,而 A中的方便 init就可以了?

26459 次浏览

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

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)
}
}

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:).

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)
}
}

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

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.

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.