Die Klasse implementiert die erforderlichen Mitglieder ihrer Oberklasse nicht

155

Deshalb habe ich heute auf Xcode 6 Beta 5 aktualisiert und festgestellt, dass ich in fast allen meiner Unterklassen der Apple-Klassen Fehler erhalten habe.

Der Fehler lautet:

Die Klasse 'x' implementiert nicht die erforderlichen Mitglieder ihrer Oberklasse

Hier ist ein Beispiel, das ich ausgewählt habe, da diese Klasse derzeit ziemlich leicht ist und daher leicht zu posten ist.

class InfoBar: SKSpriteNode  { //Error message here

    let team: Team
    let healthBar: SKSpriteNode

    init(team: Team, size: CGSize) {
        self.team = team
        if self.team == Team.TeamGood {
            healthBar = SKSpriteNode(color: UIColor.greenColor(), size:size)
        }
        else {
            healthBar = SKSpriteNode(color: UIColor.redColor(), size:size)
        }
        super.init(texture:nil, color: UIColor.darkGrayColor(), size: size)

        self.addChild(healthBar)

    }

}

Meine Frage ist also, warum ich diesen Fehler erhalte und wie ich ihn beheben kann. Was implementiere ich nicht? Ich rufe einen bestimmten Initialisierer auf.

Episches Byte
quelle

Antworten:

127

Von einem Apple-Mitarbeiter in den Entwicklerforen:

"Eine Möglichkeit, dem Compiler und dem erstellten Programm zu erklären, dass Sie wirklich nicht NSCoding-kompatibel sein möchten, besteht darin, Folgendes zu tun:"

required init(coder: NSCoder) {
  fatalError("NSCoding not supported")
}

Wenn Sie wissen, dass Sie nicht NSCoding-kompatibel sein möchten, ist dies eine Option. Ich habe diesen Ansatz mit einem Großteil meines SpriteKit-Codes gewählt, da ich weiß, dass ich ihn nicht von einem Storyboard laden werde.


Eine andere Option, die Sie nutzen können und die recht gut funktioniert, besteht darin, die Methode wie folgt als Convenience-Init zu implementieren:

convenience required init(coder: NSCoder) {
    self.init(stringParam: "", intParam: 5)
}

Beachten Sie den Aufruf eines Initialisierers in self. Auf diese Weise müssen Sie im Gegensatz zu allen nicht optionalen Eigenschaften nur Dummy-Werte für die Parameter verwenden, ohne einen schwerwiegenden Fehler auszulösen.


Die dritte Möglichkeit besteht natürlich darin, die Methode beim Aufrufen von super zu implementieren und alle nicht optionalen Eigenschaften zu initialisieren. Sie sollten diesen Ansatz wählen, wenn es sich bei dem Objekt um eine Ansicht handelt, die aus einem Storyboard geladen wird:

required init(coder aDecoder: NSCoder!) {
    foo = "some string"
    bar = 9001

    super.init(coder: aDecoder)
}
Ben Kane
quelle
3
Die zweite Option ist jedoch in den meisten Fällen im wirklichen Leben nutzlos. Nehmen Sie zum Beispiel meinen gewünschten Initialisierer init(collection:MPMediaItemCollection). Sie müssen eine echte Sammlung von Medienelementen bereitstellen. das ist der Punkt dieser Klasse. Diese Klasse kann einfach nicht ohne eine instanziiert werden. Es wird die Sammlung analysieren und ein Dutzend Instanzvariablen initialisieren. Das ist der springende Punkt, wenn dies der einzige designierte Initialisierer ist! Daher gibt es init(coder:)hier keine aussagekräftige (oder sogar bedeutungslose) MPMediaItemCollection; Nur der fatalErrorAnsatz ist richtig.
Matt
@matt Richtig, die eine oder andere Option funktioniert in verschiedenen Situationen besser.
Ben Kane
Richtig, und ich habe die zweite Option unabhängig entdeckt und in Betracht gezogen, und manchmal macht sie Sinn. Zum Beispiel hätte ich mein di deklarieren können init(collection:MPMediaItemCollection!). Das würde erlauben init(coder:), null zu passieren. Aber dann wurde mir klar: Nein, jetzt täuschen Sie nur den Compiler. Null zu übergeben ist nicht akzeptabel, also werfen Sie das fatalErrorund fahren Sie fort. :)
Matt
1
Ich weiß, dass diese Frage und ihre Antworten jetzt ziemlich alt sind, aber ich habe eine neue Antwort veröffentlicht, die einige Punkte anspricht, die meiner Meinung nach entscheidend sind, um diesen Fehler tatsächlich zu verstehen, die in keiner der vorhandenen Antworten angesprochen wurden.
nhgrif
Gute Antwort. Ich stimme Ihnen zu, dass das Verständnis, dass Swift nicht immer Superinitialisierer erbt, für das Verständnis dieses Musters von entscheidender Bedeutung ist.
Ben Kane
71

In den vorhandenen Antworten fehlen zwei absolut wichtige Swift-spezifische Informationen, die meiner Meinung nach dazu beitragen, dies vollständig zu klären.

  1. Wenn ein Protokoll einen Initialisierer als erforderliche Methode angibt, muss dieser Initialisierer mit dem requiredSchlüsselwort Swift markiert werden .
  2. Swift hat spezielle Vererbungsregeln für initMethoden.

Das tl; dr ist das:

Wenn Sie Initialisierer implementieren, erben Sie keinen der von der Oberklasse festgelegten Initialisierer mehr.

Die einzigen Initialisierer, die Sie erben werden, sind erstklassige Convenience-Initialisierer, die auf einen bestimmten Initialisierer verweisen, den Sie zufällig überschrieben haben.

Also ... bereit für die lange Version?


Swift hat spezielle Vererbungsregeln für initMethoden.

Ich weiß, dass dies der zweite von zwei Punkten war, die ich angesprochen habe, aber wir können den ersten Punkt nicht verstehen oder warum das requiredSchlüsselwort überhaupt existiert, bis wir diesen Punkt verstanden haben. Sobald wir diesen Punkt verstanden haben, wird der andere ziemlich offensichtlich.

Alle Informationen, die ich in diesem Abschnitt dieser Antwort behandele, stammen aus der Dokumentation von Apple, die Sie hier finden .

Aus den Apple-Dokumenten:

Im Gegensatz zu Unterklassen in Objective-C erben Swift-Unterklassen standardmäßig nicht ihre Superklassen-Initialisierer. Der Ansatz von Swift verhindert eine Situation, in der ein einfacher Initialisierer aus einer Oberklasse von einer spezialisierteren Unterklasse geerbt und zum Erstellen einer neuen Instanz der Unterklasse verwendet wird, die nicht vollständig oder korrekt initialisiert ist.

Hervorhebung von mir.

Direkt aus den Apple-Dokumenten sehen wir also, dass Swift-Unterklassen die initMethoden ihrer Oberklasse nicht immer (und normalerweise nicht) erben .

Wann erben sie von ihrer Oberklasse?

Es gibt zwei Regeln, die definieren, wann eine Unterklasse initMethoden von ihrem übergeordneten Element erbt . Aus den Apple-Dokumenten:

Regel 1

Wenn Ihre Unterklasse keine festgelegten Initialisierer definiert, erbt sie automatisch alle von der Oberklasse festgelegten Initialisierer.

Regel 2

Wenn Ihre Unterklasse eine Implementierung aller von der Oberklasse festgelegten Initialisierer bereitstellt - entweder durch Erben gemäß Regel 1 oder durch Bereitstellen einer benutzerdefinierten Implementierung als Teil ihrer Definition -, erbt sie automatisch alle Komfortklassen-Initialisierer der Oberklasse.

Regel 2 ist nicht besonders relevant für dieses Gespräch , weil SKSpriteNode‚s init(coder: NSCoder)ist unwahrscheinlich , dass eine bequeme Methode sein.

Ihre InfoBarKlasse hat also den requiredInitialisierer bis zu dem Punkt geerbt, den Sie hinzugefügt haben init(team: Team, size: CGSize).

Wenn Sie diese nicht zur Verfügung gestellt haben , waren initVerfahren und stattdessen auf Ihre InfoBar's hinzugefügt Eigenschaften optional oder versehen sie mit Standardwerten, dann würden Sie noch vererbt worden SKSpriteNodeist init(coder: NSCoder). Als wir jedoch unseren eigenen benutzerdefinierten Initialisierer hinzufügten, hörten wir auf, die festgelegten Initialisierer unserer Oberklasse zu erben (und praktische Initialisierer, die nicht auf von uns implementierte Initialisierer hinwiesen).

Als vereinfachtes Beispiel präsentiere ich Folgendes:

class Foo {
    var foo: String
    init(foo: String) {
        self.foo = foo
    }
}

class Bar: Foo {
    var bar: String
    init(foo: String, bar: String) {
        self.bar = bar
        super.init(foo: foo)
    }
}


let x = Bar(foo: "Foo")

Welches den folgenden Fehler darstellt:

Fehlendes Argument für Parameter 'bar' im Aufruf.

Geben Sie hier die Bildbeschreibung ein

Wenn dies Objective-C wäre, hätte es kein Problem zu erben. Wenn wir a Barmit initWithFoo:in Objective-C initialisieren self.barwürden , wäre die Eigenschaft einfach nil. Es ist wahrscheinlich nicht großartig, aber es ist ein vollkommen gültiger Zustand für das Objekt. Es ist kein vollkommen gültiger Zustand für das Swift-Objekt. Es self.barist nicht optional und kann nicht sein nil.

Wiederum erben wir Initialisierer nur, indem wir keine eigenen bereitstellen. Also , wenn wir versuchen , zu vererben durch Löschen Bars‘ init(foo: String, bar: String), wie zum Beispiel:

class Bar: Foo {
    var bar: String
}

Jetzt sind wir wieder beim Erben (irgendwie), aber das wird nicht kompiliert ... und die Fehlermeldung erklärt genau, warum wir keine initMethoden der Oberklasse erben :

Problem: Die Klasse 'Bar' hat keine Initialisierer

Fix-It: Die gespeicherte Eigenschaftsleiste ohne Initialisierer verhindert synthetisierte Initialisierer

Wenn wir gespeicherte Eigenschaften in unsere Unterklasse aufgenommen haben, gibt es keine schnelle Möglichkeit, eine gültige Instanz unserer Unterklasse mit den Superklassen-Initialisierern zu erstellen, die möglicherweise nichts über die gespeicherten Eigenschaften unserer Unterklasse wissen.


Okay, warum muss ich überhaupt implementieren init(coder: NSCoder)? Warum ist es required?

Swifts initMethoden spielen möglicherweise nach einem speziellen Satz von Vererbungsregeln, aber die Protokollkonformität wird weiterhin in der gesamten Kette vererbt. Wenn eine übergeordnete Klasse einem Protokoll entspricht, müssen ihre Unterklassen diesem Protokoll entsprechen.

Normalerweise ist dies kein Problem, da für die meisten Protokolle nur Methoden erforderlich sind, die in Swift nicht nach speziellen Vererbungsregeln abgespielt werden. Wenn Sie also von einer Klasse erben, die einem Protokoll entspricht, erben Sie auch alle Methoden oder Eigenschaften, mit denen die Klasse die Protokollkonformität erfüllen kann.

Denken Sie jedoch daran, dass Swifts initMethoden nach einem bestimmten Regelwerk gespielt werden und nicht immer vererbt werden. Aus diesem Grund erfordert eine Klasse, die einem Protokoll entspricht, das spezielle initMethoden erfordert (z. B. NSCoding), dass die Klasse diese initMethoden als markiert required.

Betrachten Sie dieses Beispiel:

protocol InitProtocol {
    init(foo: Int)
}

class ConformingClass: InitProtocol {
    var foo: Int
    init(foo: Int) {
        self.foo = foo
    }
}

Dies wird nicht kompiliert. Es wird die folgende Warnung generiert:

Problem: Die Initialisierungsanforderung 'init (foo :)' kann nur von einem 'erforderlichen' Initialisierer in der nicht endgültigen Klasse 'ConformingClass' erfüllt werden.

Fix-It: Einfügen erforderlich

Ich soll den init(foo: Int)erforderlichen Initialisierer erstellen. Ich könnte es auch glücklich machen, indem ich die Klasse mache final(was bedeutet, dass die Klasse nicht geerbt werden kann).

Was passiert also, wenn ich eine Unterklasse habe? Ab diesem Punkt geht es mir gut, wenn ich eine Unterklasse habe. Wenn ich jedoch Initialisierer hinzufüge, erbe ich plötzlich nicht mehr init(foo:). Das ist problematisch, weil ich mich jetzt nicht mehr an die anpasse InitProtocol. Ich kann keine Unterklasse einer Klasse bilden, die einem Protokoll entspricht, und dann plötzlich entscheiden, dass ich diesem Protokoll nicht mehr entsprechen möchte. Ich habe die Protokollkonformität geerbt, aber aufgrund der Art und Weise, wie Swift mit der initMethodenvererbung arbeitet, habe ich keinen Teil dessen geerbt, was zur Konformität mit diesem Protokoll erforderlich ist, und ich muss es implementieren.


Okay, das alles macht Sinn. Aber warum kann ich keine hilfreichere Fehlermeldung erhalten?

Möglicherweise ist die Fehlermeldung klarer oder besser, wenn angegeben wird, dass Ihre Klasse nicht mehr dem geerbten NSCodingProtokoll entspricht und dass Sie sie implementieren müssen, um sie zu beheben init(coder: NSCoder). Sicher.

Xcode kann diese Nachricht jedoch einfach nicht generieren, da dies nicht immer das eigentliche Problem ist, wenn eine erforderliche Methode nicht implementiert oder geerbt wird. Neben der Protokollkonformität gibt es noch einen weiteren Grund, initMethoden zu requirederstellen, und das sind Factory-Methoden.

Wenn ich eine richtige Factory-Methode schreiben möchte, muss ich den Rückgabetyp angeben Self(Swifts Äquivalent zu Objective-Cs instanceType). Dazu muss ich jedoch eine requiredInitialisierungsmethode verwenden.

class Box {
    var size: CGSize
    init(size: CGSize) {
        self.size = size
    }

    class func factory() -> Self {
        return self.init(size: CGSizeZero)
    }
}

Dies erzeugt den Fehler:

Das Konstruieren eines Objekts vom Klassentyp 'Self' mit einem Metatypwert muss einen 'erforderlichen' Initialisierer verwenden

Geben Sie hier die Bildbeschreibung ein

Es ist im Grunde das gleiche Problem. Wenn wir eine Unterklasse bilden Box, erben unsere Unterklassen die Klassenmethode factory. Also konnten wir anrufen SubclassedBox.factory(). Ohne das requiredSchlüsselwort in der init(size:)Methode wird Boxjedoch nicht garantiert, self.init(size:)dass factorydie Unterklassen des Aufrufs das erben , was aufgerufen wird.

Wir müssen diese Methode also erstellen, requiredwenn wir eine Factory-Methode wie diese wollen. Wenn unsere Klasse eine requiredsolche Methode implementiert, haben wir eine Initialisierungsmethode und stoßen auf genau dieselben Probleme, auf die Sie hier gestoßen sind mit dem NSCodingProtokoll.


Letztendlich läuft alles auf das grundlegende Verständnis hinaus, dass Swifts Initialisierer nach etwas anderen Vererbungsregeln spielen, was bedeutet, dass Sie nicht garantiert Initialisierer von Ihrer Oberklasse erben. Dies liegt daran, dass Superklassen-Initialisierer Ihre neuen gespeicherten Eigenschaften nicht kennen und Ihr Objekt nicht in einen gültigen Zustand versetzen können. Aus verschiedenen Gründen kann eine Superklasse einen Initialisierer als markieren required. In diesem Fall können wir entweder eines der sehr spezifischen Szenarien verwenden, mit denen wir die requiredMethode tatsächlich erben , oder wir müssen sie selbst implementieren.

Der Hauptpunkt hier ist jedoch, dass wenn wir den Fehler sehen, den Sie hier sehen, dies bedeutet, dass Ihre Klasse die Methode überhaupt nicht implementiert.

initBetrachten Sie dieses Beispiel als ein letztes Beispiel, um die Tatsache zu untersuchen, dass Swift-Unterklassen nicht immer die Methoden ihrer Eltern erben (was meiner Meinung nach für das vollständige Verständnis dieses Problems von zentraler Bedeutung ist):

class Foo {
    init(a: Int, b: Int, c: Int) {
        // do nothing
    }
}

class Bar: Foo {
    init(string: String) {
        super.init(a: 0, b: 1, c: 2)
        // do more nothing
    }
}

let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)

Dies kann nicht kompiliert werden.

Geben Sie hier die Bildbeschreibung ein

Die Fehlermeldung ist etwas irreführend:

Zusätzliches Argument 'b' im Aufruf

Aber der Punkt ist, Barerbt nicht jeden Foo‚s initMethoden , weil sie nicht eine der beiden Sonderfälle für Vererbungs erfüllt haben initMethoden von der übergeordneten Klasse.

Wenn dies Objective-C wäre, würden wir dies initohne Probleme erben , da Objective-C vollkommen glücklich ist, die Eigenschaften von Objekten nicht zu initialisieren (obwohl Sie als Entwickler damit nicht zufrieden sein sollten). In Swift reicht dies einfach nicht aus. Sie können keinen ungültigen Status haben, und das Erben von Superklassen-Initialisierern kann nur zu ungültigen Objektzuständen führen.

nhgrif
quelle
Können Sie bitte erklären, was dieser Satz bedeutet, oder ein Beispiel geben? "(und Convenience-Initialisierer, die nicht auf von uns implementierte Initialisierer verweisen)"
Abbey Jackson,
Geniale Antwort! Ich wünsche mir mehr SO-Beiträge darüber, warum , wie dieser, anstatt nur wie .
Alexander Vasenin
56

Warum ist dieses Problem aufgetreten? Nun, die klare Tatsache ist, dass es immer wichtig war (dh in Objective-C, seit ich mit dem Programmieren von Cocoa in Mac OS X 10.0 begonnen habe), sich mit Initialisierern zu befassen, auf die Ihre Klasse nicht vorbereitet ist. In den Dokumenten war Ihre diesbezügliche Verantwortung immer klar. Aber wie viele von uns haben sich die Mühe gemacht, sie vollständig und schriftlich zu erfüllen? Wahrscheinlich keiner von uns! Und der Compiler hat sie nicht durchgesetzt; es war alles rein konventionell.

Beispiel: In meiner Objective-C-View-Controller-Unterklasse mit diesem festgelegten Initialisierer:

- (instancetype) initWithCollection: (MPMediaItemCollection*) coll;

... ist es entscheidend, dass uns eine tatsächliche Sammlung von Medienobjekten übergeben wird: Die Instanz kann ohne eine solche einfach nicht entstehen. Aber ich habe keinen "Stopper" geschrieben, um zu verhindern, dass mich jemand initstattdessen mit bloßen Knochen initialisiert . Ich hätte einen schreiben sollen (eigentlich hätte ich eigentlich eine Implementierung des initWithNibName:bundle:geerbten designierten Initialisierers schreiben sollen ); aber ich war zu faul, um mich darum zu kümmern, weil ich "wusste", dass ich meine eigene Klasse niemals falsch auf diese Weise initialisieren würde. Dies hinterließ ein klaffendes Loch. In Objective-C, jemand kann nennen nackte Knochen init, so dass mein ivars nicht initialisiert, und wir sind auf den Bach ohne Paddel.

Swift rettet mich in den meisten Fällen wunderbar vor mir selbst. Sobald ich diese App in Swift übersetzt hatte, verschwand das ganze Problem. Swift schafft effektiv einen Stopper für mich! Wenn init(collection:MPMediaItemCollection)der einzige festgelegte Initialisierer in meiner Klasse deklariert ist, kann ich nicht durch Aufrufen von Bare-Bones initialisiert werden init(). Es ist ein Wunder!

Was in Seed 5 passiert ist, ist lediglich, dass der Compiler erkannt hat, dass das Wunder im Fall von nicht funktioniert init(coder:), weil theoretisch eine Instanz dieser Klasse von einer Feder stammen könnte und der Compiler dies nicht verhindern kann - und wann die Federlasten init(coder:)werden aufgerufen. Der Compiler lässt Sie den Stopper also explizit schreiben. Und ganz richtig auch.

matt
quelle
Vielen Dank für diese ausführliche Antwort. Dies bringt wirklich Licht in die Sache.
Julian Osorio
Ein Upvote zu pasta12, um mir zu sagen, wie man den Compiler zum Schweigen bringt, aber auch ein Upvote zu Ihnen, um mich darauf hinzuweisen, worüber er überhaupt gejammert hat.
Garrett Albright
2
Gaping Hole oder nicht, ich wollte diesen Init nie nennen, daher ist es absolut beleidigend, mich zu zwingen, ihn aufzunehmen. Aufgeblähter Code ist ein Aufwand, den keiner von uns benötigt. Es zwingt Sie jetzt auch dazu, Ihre Eigenschaften auch in beiden Inits zu initialisieren. Zwecklos!
Dan Greenfield
5
@DanGreenfield Nein, es zwingt Sie nicht, etwas zu initialisieren, denn wenn Sie es nie aufrufen wollen, setzen Sie einfach den unter stackoverflow.com/a/25128815/341994fatalError beschriebenen Stopper ein . Machen Sie es einfach zu einem Benutzer-Code-Snippet und von nun an können Sie es einfach dort platzieren, wo es benötigt wird. Dauert eine halbe Sekunde.
Matt
1
@nhgrif Nun, um fair zu sein, die Frage hat nicht nach der vollständigen Hintergrundgeschichte gefragt. Es ging nur darum, aus dieser Marmelade herauszukommen und weiterzumachen. Die ganze Geschichte ist in meinem Buch enthalten: apeth.com/swiftBook/ch04.html#_class_initializers
matt
33

hinzufügen

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
quelle
3
Das funktioniert, aber ich denke nicht, dass es ein Fehler ist. Initialisierer werden nicht schnell vererbt (wenn Ihr eigener Initialisierer deklariert ist) und dies wird mit dem erforderlichen Schlüsselwort markiert. Das einzige Problem ist, dass ich jetzt ALLE meine Eigenschaften in dieser Methode für jede meiner Klassen initialisieren muss, was eine Menge verschwendeten Codes bedeutet, da ich dies überhaupt nicht verwende. Oder ich muss alle meine Eigenschaften als implizit entpackte optionale Typen deklarieren, um die Initialisierung zu umgehen, die ich auch nicht tun möchte.
Epic Byte
1
Jep! Sobald ich sagte, dass es ein Fehler sein könnte, wurde mir klar, dass es tatsächlich logisch sinnvoll ist. Ich bin damit einverstanden, dass viel Code verschwendet wird, da ich diese Init-Methode wie Sie niemals verwenden würde. Ich bin mir noch nicht sicher über eine elegante Lösung
Gagan Singh
2
Ich hatte das gleiche Problem. Es macht Sinn mit "Required Init", aber Swift ist nicht die "einfache" Sprache, auf die ich gehofft habe. All diese "optionalen" machen die Sprache komplexer als erforderlich. Und keine Unterstützung für DSL und AOP. Ich werde immer enttäuschter.
user810395
2
Ja, ich stimme vollkommen zu. Viele meiner Immobilien werden jetzt als optional deklariert, weil ich dazu gezwungen bin, wenn sie eigentlich nicht null sein dürfen. Einige sind optional, weil sie zu Recht optional sein sollten (was bedeutet, dass Null ein gültiger Wert ist). Und dann muss ich in Klassen, in denen ich keine Unterklassen habe, keine Optionen verwenden, daher werden die Dinge sehr komplex und ich kann anscheinend keinen richtigen Codierungsstil finden. Hoffentlich findet Apple etwas heraus.
Epic Byte
5
Ich denke, sie bedeuten, dass Sie den erforderlichen Initialisierer erfüllen können, indem Sie keinen eigenen Initialisierer deklarieren, was dazu führen würde, dass alle Initialisierer vererbt werden.
Epic Byte