为什么Swift初始化程序不能在其超类上调用便捷初始化程序?


88

考虑两个类:

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再次,当方便initA会做得很好?


2
我搜索了一个答案,但找不到任何能让我满意的答案。这可能是一些实现原因。也许在另一个类中搜索指定的初始化程序要比搜索便捷的初始化程序容易得多。
苏珊(Sulthan)2014年

@Robert,感谢您在下面的评论。我认为您可以将它们添加到您的问题中,或者发布一个收到的答案:“这是设计使然,并且在此区域中已解决了所有相关的错误。” 因此,看起来他们不能或不想解释原因。
Ferran Maylinch '17

Answers:


24

这是《 Swift编程指南》中指定的“初始化链接”规则的规则1,内容为:

规则1:指定的初始化程序必须从其直接超类调用指定的初始化程序。

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Initialization.html

强调我的。指定的初始化程序无法调用便捷初始化程序。

有一张图和规则一起演示了允许使用哪些初始化程序“方向”:

初始化链


80
但是为什么要这样呢?该文档只是说它简化了设计,但是如果我不得不通过不断在指定的初始化器上指定默认值来重复自己,我不知道会怎样,我大多不在乎何时使用默认值。初始化器会做什么?
罗伯特

5
在提出此问题后的第二天,我提交了一个错误17266917,要求能够调用便捷初始化程序。尚无回应,但我也没有其他任何Swift错误报告!
罗伯特

8
希望他们将允许致电便利初始化程序。对于SDK中的某些类,除了调用便捷初始化程序外,没有其他方法可以实现某种行为。请参阅SCNGeometry:您SCNGeometryElement只能使用便捷初始化程序添加,因此不能从中继承。
Spak 2014年

4
这是一个非常糟糕的语言设计决策。从我的新应用程序一开始,我就决定快速开发,我需要从我的子类中调用NSWindowController.init(windowNibName),但我做不到:(
Uniqus 2014年

4
@Kaiserludi:没什么用,它被关闭并显示以下响应:“这是设计使然,已经解决了此区域中的所有相关错误。”
罗伯特

21

考虑

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

因为重写了所有指定的类A的初始值设定项,所以类B将继承A的所有便利性初始值设定项。因此执行此输出

Entering A.init()
Entering A.init(a:)
Entering B.init(a:,b:)
Entering A.init(a:,b:)

现在,如果允许指定的初始化程序B.init(a:b :)调用基类便捷性初始化程序A.init(a :),则将导致对B.init(a:,b:的递归调用: )。


这很容易解决。一旦从指定的初始化程序中调用超类的便利初始化器,该超类的便利初始化器将不再被继承。
fluidsonic

@fluidsonic,但这将是疯狂的,因为您的结构和类方法将根据使用方式而发生变化。想象一下调试的乐趣!
kdazzle '16

2
@kdazzle类的结构及其类方法都不会改变。他们为什么要这样?-我能想到的唯一问题是,便利初始化器在继承时必须动态地委派给子类的指定初始化器,而在未继承但从子类委托时必须静态地委托给自己类的指定初始化器。
fluidsonic

我认为您也可以使用普通方法获得类似的递归问题。这取决于您在调用方法时的决定。例如,如果一种语言不允许递归调用,那将很愚蠢,因为您可能会陷入无限循环。程序员应该了解他们在做什么。:)
Ferran Maylinch's

13

这是因为您可以得到无限递归。考虑:

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

并查看:

let a = SubClass()

哪个会打电话SubClass.init(),哪个会打电话SuperClass.init(value:),哪个会打电话SubClass.init()

指定/便利初始化规则旨在使类初始化始终正确。


1
尽管子类可能不会从其父类显式调用便捷初始化器,但是它可以继承它们,因为子类会提供其所有父类指定的初始化器的实现(例如,本示例)。因此,您上面的示例确实是正确的,但是是一个特殊情况,它与超类的便捷初始化程序没有明确的关系,而是与指定的初始化程序可能不会调用便捷的事实有关,因为这将导致诸如上述的递归方案。 。
dfri

1

我为此找到了解决方法。它不是超级漂亮,但是它解决了不知道超类的值或想要设置默认值的问题。

您所要做的就是init直接在init子类的中使用方便创建超类的实例。然后init,使用刚刚创建的实例调用超级对象的指定对象。

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

1

考虑从方便的位置提取初始化代码init()到新的helper函数foo()foo(...)在您的子类中调用进行初始化。


很好的建议,但实际上并不能回答问题。
SwiftsNamesake

0

请看18:30的WWDC视频“ 403中间Swift”,深入了解初始化程序及其继承。据我了解,请考虑以下几点:

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

但是现在考虑一个Wyrm子类:Wyrm是没有腿也没有翅膀的龙。因此,飞龙(2条腿,2条翅膀)的初始化器是错误的!如果无法简单地调用便捷的Wyvern-Initializer,而只能调用完整指定的Initializer,则可以避免该错误:

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

12
那不是真正的原因。如果在initWyvern有意义的时候我创建一个子类怎么办?
Sulthan 2014年

4
是的,我不相信。Wyrm调用便捷初始化程序后,没有任何事情可以覆盖其分支数。
罗伯特

这是WWDC视频中给出的原因(仅适用于汽车->赛车和布尔型hasTurbo属性)。如果子类实现了所有指定的初始化器,则它也继承便利初始化器。我有点理解它的含义,老实说,我对Objective-C的工作方式也没有多大麻烦。另请参见新约定,在init中最后调用super.init,而不是像在Objective-C中那样首先调用。
2014年

IMO将super.init称为“ last”的约定在概念上并不是新的,它与Objective-C中的约定相同,只是在那里,所有东西都被自动分配了初始值(nil,0,无论如何)。只是在Swift中,我们必须自己进行此阶段的初始化。这种方法的好处是我们还可以选择分配其他初始值。
Roshan 2014年

-1

您为什么不只有两个初始化程序-一个具有默认值?

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.