Warum können Swift-Initialisierer in ihrer Oberklasse keine Convenience-Initialisierer aufrufen?

88

Betrachten Sie die beiden Klassen:

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

Ich verstehe nicht, warum das nicht erlaubt ist. Letztendlich wird der von jeder Klasse festgelegte Initialisierer mit allen benötigten Werten aufgerufen. Warum muss ich mich also wiederholen B, initindem ich einen Standardwert für xerneut spezifiziere , wenn die Bequemlichkeit initin Agut ist?

Robert
quelle
2
Ich habe nach einer Antwort gesucht, aber ich kann keine finden, die mich zufriedenstellen würde. Dies ist wahrscheinlich ein Grund für die Implementierung. Vielleicht ist die Suche nach bestimmten Initialisierern in einer anderen Klasse viel einfacher als die Suche nach praktischen Initialisierern ... oder so ähnlich.
Sulthan
@ Robert, danke für deine Kommentare unten. Ich denke, Sie könnten sie Ihrer Frage hinzufügen oder sogar eine Antwort mit dem, was Sie erhalten haben, posten: "Dies ist beabsichtigt und alle relevanten Fehler wurden in diesem Bereich behoben." Es sieht also so aus, als könnten oder wollen sie den Grund nicht erklären.
Ferran Maylinch

Antworten:

24

Dies ist Regel 1 der Regeln für die "Initializer-Verkettung", wie im Swift-Programmierhandbuch angegeben:

Regel 1: Bestimmte Initialisierer müssen einen bestimmten Initialisierer aus ihrer unmittelbaren Oberklasse aufrufen .

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

Hervorhebung von mir. Bestimmte Initialisierer können keine praktischen Initialisierer aufrufen.

Es gibt ein Diagramm, das den Regeln entspricht, um zu demonstrieren, welche "Anweisungen" des Initialisierers zulässig sind:

Verkettung des Initialisierers

Craig Otis
quelle
79
Aber warum wird es so gemacht? Die Dokumentation sagt nur, dass es das Design vereinfacht, aber ich sehe nicht, wie dies der Fall ist, wenn ich mich ständig wiederholen muss, indem ich kontinuierlich Standardwerte für bestimmte Initialisierer spezifiziere, die mir meistens egal sind, wenn die Standardwerte der Einfachheit halber Initialisierer reichen aus?
Robert
5
Ein paar Tage nach dieser Frage habe ich einen Fehler 17266917 eingereicht und gefragt, ob ich Convenience-Initialisierer aufrufen kann. Noch keine Antwort, aber ich habe auch keine für meine anderen Swift-Fehlerberichte erhalten!
Robert
8
Hoffentlich können sie Convenience-Initialisierer aufrufen. Für einige Klassen im SDK gibt es keine andere Möglichkeit, ein bestimmtes Verhalten zu erreichen, als den Convenience-Initialisierer aufzurufen. Siehe SCNGeometry: Sie können SCNGeometryElements nur mit dem Convenience-Initialisierer hinzufügen , daher kann man nicht davon erben.
Spak
4
Dies ist eine sehr schlechte Entscheidung für das Sprachdesign. Von Anfang an habe ich mich entschlossen, schnell zu entwickeln. Ich muss NSWindowController.init (windowNibName) aus meiner Unterklasse aufrufen, und das kann ich einfach nicht :(
Uniqus
4
@Kaiserludi: Nichts Nützliches, es wurde mit der folgenden Antwort geschlossen: "Dies ist beabsichtigt und alle relevanten Fehler wurden in diesem Bereich behoben."
Robert
21

Erwägen

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

Da alle festgelegten Initialisierer der Klasse A überschrieben werden, erbt die Klasse B alle praktischen Initialisierer von A. Wenn Sie dies ausführen, wird dies ausgegeben

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

Wenn der angegebene Initialisierer B.init (a: b :) den Basisklassen-Convenience-Initialisierer A.init (a :) aufrufen darf, führt dies zu einem rekursiven Aufruf von B.init (a :, b :. ).

beba
quelle
Das ist einfach zu umgehen. Sobald Sie den Convenience-Initialisierer einer Superklasse aus einem bestimmten Initialisierer heraus aufrufen, werden die Convenience-Initialisierer der Superklasse nicht mehr vererbt.
Fluidsonic
@fluidsonic, aber das wäre verrückt, da sich Ihre Struktur- und Klassenmethoden je nach Verwendung ändern würden. Stellen Sie sich den Debugging-Spaß vor!
kdazzle
2
@kdazzle Weder die Struktur der Klasse noch ihre Klassenmethoden würden sich ändern. Warum sollten sie? - Das einzige Problem, an das ich denken kann, ist, dass Convenience-Initialisierer bei der Vererbung dynamisch an den festgelegten Initialisierer einer Unterklasse und statisch an den festgelegten Initialisierer der eigenen Klasse delegieren müssen, wenn sie nicht geerbt, sondern an eine Unterklasse delegiert werden.
Fluidsonic
Ich denke, Sie können mit normalen Methoden auch ein ähnliches Rekursionsproblem bekommen. Das hängt von Ihrer Entscheidung beim Aufrufen von Methoden ab. Zum Beispiel wäre es albern, wenn eine Sprache keine rekursiven Aufrufe zulässt, weil Sie in eine Endlosschleife geraten könnten. Programmierer sollten verstehen, was sie tun. :)
Ferran Maylinch
13

Es ist, weil Sie mit einer unendlichen Rekursion enden können. Erwägen:

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

und schau dir an:

let a = SubClass()

was wird anrufen SubClass.init() welches anruft SuperClass.init(value:)welches anruft SubClass.init().

Die festgelegten / praktischen Initialisierungsregeln sind so konzipiert, dass eine Klasseninitialisierung immer korrekt ist.

Julien
quelle
1
Während eine Unterklasse Convenience-Initialisierer möglicherweise nicht explizit von ihrer Oberklasse aufruft, kann sie diese erben , da die Unterklasse die Implementierung aller von ihrer Oberklasse bezeichneten Initialisierer liefert (siehe z . B. dieses Beispiel ). Daher ist Ihr obiges Beispiel zwar wahr, aber ein Sonderfall, der nicht explizit mit dem Convenience-Initialisierer einer Oberklasse zusammenhängt, sondern mit der Tatsache, dass ein bestimmter Initialisierer möglicherweise keinen Convenience- Initialisierer aufruft , da dies zu rekursiven Szenarien wie dem oben genannten führt .
dfri
1

Ich habe eine Lösung dafür gefunden. Es ist nicht besonders hübsch, aber es löst das Problem, die Werte einer Oberklasse nicht zu kennen oder Standardwerte festlegen zu wollen.

Alles, was Sie tun müssen, ist, eine Instanz der Oberklasse zu erstellen, indem Sie die Bequemlichkeit initdirekt in initder Unterklasse verwenden. Dann rufen Sie den designierten initSuper mit der gerade erstellten Instanz auf.

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) 
    }
}
bigelerow
quelle
1

Ziehen Sie in Betracht, den Initialisierungscode von Ihrer praktischen init()zu einer neuen Hilfsfunktion zu extrahieren foo(), und rufen Sie foo(...)auf, um die Initialisierung in Ihrer Unterklasse durchzuführen.

Evanchin
quelle
Guter Vorschlag, aber er beantwortet die Frage nicht wirklich.
SwiftsNamesake
0

Schauen Sie sich das WWDC-Video "403 Intermediate Swift" um 18:30 an, um eine ausführliche Erklärung der Initialisierer und ihrer Vererbung zu erhalten. Beachten Sie nach meinem Verständnis Folgendes:

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

Aber jetzt betrachten wir eine Wyrm-Unterklasse: Ein Wyrm ist ein Drache ohne Beine und ohne Flügel. Der Initializer für einen Wyvern (2 Beine, 2 Flügel) ist also falsch! Dieser Fehler kann vermieden werden, wenn der bequeme Wyvern-Initializer einfach nicht aufgerufen werden kann, sondern nur der vollständig festgelegte Initializer:

class Wyrm: Dragon {
    init() {
        super.init(legs: 0, isFlying: false)
    }
}
Ralf
quelle
12
Das ist nicht wirklich ein Grund. Was ist, wenn ich eine Unterklasse initWyvernerstelle, wenn es Sinn macht, angerufen zu werden?
Sulthan
4
Ja, ich bin nicht überzeugt. Es hindert nichts daran, Wyrmdie Anzahl der Beine zu überschreiben, nachdem ein Convenience-Initialisierer aufgerufen wurde.
Robert
Dies ist der im WWDC-Video angegebene Grund (nur bei Autos -> Rennwagen und einer booleschen hasTurbo-Eigenschaft). Wenn die Unterklasse alle festgelegten Initialisierer implementiert, erbt auch sie die praktischen Initialisierer. Ich sehe den Sinn darin, außerdem hatte ich ehrlich gesagt keine großen Probleme mit der Funktionsweise von Objective-C. Siehe auch die neue Konvention zum Aufrufen von super.init als letztes in der Init, nicht zuerst wie in Objective-C.
Ralf
IMO ist die Konvention, super.init "last" zu nennen, konzeptionell nicht neu, sie ist die gleiche wie in Ziel-c, nur dass dort automatisch allen ein Anfangswert (null, 0, was auch immer) zugewiesen wurde. Es ist nur so, dass wir in Swift diese Phase der Initialisierung selbst durchführen müssen. Ein Vorteil dieses Ansatzes ist, dass wir auch die Möglichkeit haben, einen anderen Anfangswert zuzuweisen.
Roshan
-1

Warum haben Sie nicht einfach zwei Initialisierer - einen mit einem Standardwert?

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
Jason
quelle