Fehler in der Swift-Klasse: Eigenschaft beim Aufruf von super.init nicht initialisiert

218

Ich habe zwei Klassen ShapeundSquare

class Shape {
    var numberOfSides = 0
    var name: String
    init(name:String) {
        self.name = name
    }
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Bei der obigen Implementierung erhalte ich den Fehler:

property 'self.sideLength' not initialized at super.init call
    super.init(name:name)

Warum muss ich einstellen, self.sideLengthbevor ich anrufe super.init?

JuJoDi
quelle
Ich bin mir ziemlich sicher, dass dies mit guten Programmierpraktiken zu tun hat, nicht mit einer tatsächlichen technischen Einschränkung. Wenn Shape eine Funktion aufruft, die Square überschrieben hat, möchte Square möglicherweise sideLength verwenden, diese wurde jedoch noch nicht initialisiert. Swift hindert Sie wahrscheinlich nur daran, dies versehentlich zu tun, indem Sie gezwungen werden, Ihre Instanzen zuerst zu initialisieren, bevor Sie die Basisklasse aufrufen.
Cwharris
3
Die Beispiele im Buch sind schlecht. Sie sollten immer zuletzt super.init () aufrufen , um sicherzustellen, dass alle Eigenschaften initialisiert wurden (siehe unten).
Pascal

Antworten:

173

Zitat aus der Programmiersprache Swift, das Ihre Frage beantwortet:

„Der Compiler von Swift führt vier hilfreiche Sicherheitsüberprüfungen durch, um sicherzustellen, dass die zweiphasige Initialisierung fehlerfrei abgeschlossen wird:“

Sicherheitsprüfung 1 „Ein bestimmter Initialisierer muss sicherstellen, dass alle von seiner Klasse eingeführten Eigenschaften initialisiert werden, bevor er an einen Initialisierer der Oberklasse delegiert.“

Auszug aus: Apple Inc. "Die schnelle Programmiersprache". iBooks. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

Ruben
quelle
47
Das ist eine erstaunliche Veränderung im Vergleich zu C ++, C # oder Java.
MDJ
12
@ MDJ Sicher. Ich sehe ehrlich gesagt auch keinen Mehrwert.
Ruben
20
Eigentlich scheint es einen zu geben. Angenommen, in C # sollte der Superklassenkonstruktor keine überschreibbaren (virtuellen) Methoden aufrufen, da niemand weiß, wie sie mit einer nicht vollständig initialisierten Unterklasse reagieren würden. In Swift ist dies in Ordnung, da der Status der zusätzlichen Unterklasse in Ordnung ist, wenn der Konstruktor der Oberklasse ausgeführt wird. Darüber hinaus sind in Swift alle bis auf die endgültigen Methoden überschreibbar.
MDJ
17
Ich finde es besonders ärgerlich, weil es bedeutet, dass ich, wenn ich eine UIView-Unterklasse erstellen möchte, die ihre eigenen Unteransichten erstellt, diese Unteransichten zunächst ohne Frames initialisieren und die Frames später hinzufügen muss, da ich nicht auf die Ansichten verweisen kann Grenzen bis NACH dem Aufruf von super.init.
Ash
5
@Janos Wenn Sie die Eigenschaft optional machen, müssen Sie sie nicht initialisieren init.
JeremyP
105

Swift hat eine sehr klare, spezifische Abfolge von Operationen, die in Initialisierern ausgeführt werden. Beginnen wir mit einigen grundlegenden Beispielen und arbeiten uns bis zu einem allgemeinen Fall vor.

Nehmen wir ein Objekt A. Wir definieren es wie folgt.

class A {
    var x: Int
    init(x: Int) {
        self.x = x
    }
}

Beachten Sie, dass A keine Superklasse hat und daher keine super.init () -Funktion aufrufen kann, da diese nicht vorhanden ist.

OK, jetzt wollen wir die Unterklasse A mit einer neuen Klasse namens B unterteilen.

class B: A {
    var y: Int
    init(x: Int, y: Int) {
        self.y = y
        super.init(x: x)
    }
}

Dies ist eine Abweichung von Objective-C, wo [super init]normalerweise zuerst vor allem anderen aufgerufen wird. Nicht so bei Swift. Sie sind dafür verantwortlich, sicherzustellen, dass sich Ihre Instanzvariablen in einem konsistenten Zustand befinden, bevor Sie etwas anderes tun, einschließlich der Aufrufmethoden (einschließlich des Initialisierers Ihrer Oberklasse).

Hitendra Solanki
quelle
1
Dies ist äußerst hilfreich, ein klares Beispiel. Danke!
FullMetalFist
Was ist, wenn ich den Wert zur Berechnung von y benötige, zum Beispiel: init (y: Int) {self.y = y * self.x super.init ()}
6rod9
1
benutze so etwas wie init (y: Int, x: Int = 0) {self.y = y * x; self.x = x; super.init (x: x)}, auch können Sie den leeren Konstruktor für die Superklasse unter Bezugnahme auf das obige Beispiel nicht direkt aufrufen, da die
Superklassennamen
43

Aus den Dokumenten

Sicherheitsüberprüfung 1

Ein designierter Initialisierer muss sicherstellen, dass alle von seiner Klasse eingeführten Eigenschaften initialisiert werden, bevor er an einen Superklassen-Initialisierer delegiert.


Warum brauchen wir so eine Sicherheitsüberprüfung?

Um dies zu beantworten, lassen Sie uns den Initialisierungsprozess schnell durchlaufen.

Zweiphaseninitialisierung

Die Klasseninitialisierung in Swift ist ein zweiphasiger Prozess. In der ersten Phase wird jeder gespeicherten Eigenschaft von der Klasse, die sie eingeführt hat, ein Anfangswert zugewiesen. Sobald der Anfangszustand für jede gespeicherte Eigenschaft bestimmt wurde, beginnt die zweite Phase, und jede Klasse hat die Möglichkeit, ihre gespeicherten Eigenschaften weiter anzupassen, bevor die neue Instanz als einsatzbereit betrachtet wird.

Die Verwendung eines zweiphasigen Initialisierungsprozesses macht die Initialisierung sicher und bietet gleichzeitig jeder Klasse in einer Klassenhierarchie vollständige Flexibilität. Die zweiphasige Initialisierung verhindert, dass auf Eigenschaftswerte zugegriffen wird, bevor sie initialisiert werden , und verhindert, dass Eigenschaftswerte von einem anderen Initialisierer unerwartet auf einen anderen Wert gesetzt werden.

Um sicherzustellen, dass der zweistufige Initialisierungsprozess wie oben definiert durchgeführt wird, gibt es vier Sicherheitsüberprüfungen. Eine davon ist:

Sicherheitsüberprüfung 1

Ein designierter Initialisierer muss sicherstellen, dass alle von seiner Klasse eingeführten Eigenschaften initialisiert werden, bevor er an einen Superklassen-Initialisierer delegiert.

Bei der zweiphasigen Initialisierung geht es nie um die Reihenfolge, aber diese Sicherheitsüberprüfung führt super.initdazu, dass nach der Initialisierung aller Eigenschaften eine Bestellung durchgeführt wird.

Die Sicherheitsüberprüfung 1 scheint irrelevant zu sein, da die zweiphasige Initialisierung verhindert, dass auf Eigenschaftswerte zugegriffen werden kann, bevor sie initialisiert werden, ohne diese Sicherheitsüberprüfung 1 erfüllt werden kann.

Wie in diesem Beispiel

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        super.init(sides: 3, named: "Triangle") 
        self.hypotenuse = hypotenuse
    }
}

Triangle.inithat jede Eigenschaft vor der Verwendung initialisiert. Sicherheitscheck 1 scheint also irrelevant,

Aber dann könnte es ein anderes Szenario geben, ein bisschen komplex,

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
        printShapeDescription()
    }
    func printShapeDescription() {
        print("Shape Name :\(self.name)")
        print("Sides :\(self.sides)")
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        self.hypotenuse = hypotenuse
        super.init(sides: 3, named: "Triangle")
    }

    override func printShapeDescription() {
        super.printShapeDescription()
        print("Hypotenuse :\(self.hypotenuse)")
    }
}

let triangle = Triangle(hypotenuse: 12)

Ausgabe :

Shape Name :Triangle
Sides :3
Hypotenuse :12

Wenn wir das super.initvor dem Setzen des aufgerufen hätten hypotenuse, hätte der super.initAufruf dann das aufgerufen, printShapeDescription()und da dies überschrieben wurde, würde er zuerst auf die Implementierung der Triangle-Klasse von zurückgreifen printShapeDescription(). Die printShapeDescription()of Triangle-Klasse greift auf hypotenuseeine nicht optionale Eigenschaft zu, die noch nicht initialisiert wurde. Dies ist nicht zulässig, da durch die zweiphasige Initialisierung verhindert wird, dass auf Eigenschaftswerte zugegriffen wird, bevor sie initialisiert werden

Stellen Sie also sicher, dass die Zwei-Phasen-Initialisierung wie definiert durchgeführt wird. Es muss eine bestimmte super.initAufrufreihenfolge geben. Nach der Initialisierung aller von der selfKlasse eingeführten Eigenschaften benötigen wir daher eine Sicherheitsüberprüfung 1

BangOperator
quelle
1
Tolle Erklärung, das Warum sollte definitiv zur Top-Antwort hinzugefügt werden.
Guy Daher
Sie sagen also im Grunde, weil die Oberklasse init möglicherweise eine (Überschreibungs-) Funktion aufruft ... in der diese Funktion auf die Unterklasseneigenschaft zugreift. Um zu vermeiden, dass keine Werte festgelegt werden, muss der Aufruf von supererfolgen, nachdem alle Werte festgelegt wurden. OK macht Sinn. Frage mich, wie Objective-C es damals gemacht hat und warum Sie superzuerst anrufen mussten ?
Honig
Im Wesentlichen ist das, worauf Sie hinweisen, ähnlich wie: Platzieren printShapeDescription() vor, self.sides = sides; self.name = named; was diesen Fehler erzeugen würde : use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. Der Fehler des OP wird angegeben, um die Möglichkeit eines Laufzeitfehlers zu verringern.
Honig
Ich habe speziell das Wort "Möglichkeit" verwendet, denn wenn printShapeDescriptiones sich um eine Funktion handelte, die sich nicht auf self"Drucken" ("nichts") bezog, hätte es keine Probleme gegeben. (Aber selbst dafür würde der Compiler einen Fehler auslösen, weil es nicht super klug ist)
Honey
Nun, objc war einfach nicht sicher. Swift ist typsicher, daher müssen Objekte, die nicht optional sind, nicht null sein!
Daij-Djan
36

Die "super.init ()" sollte aufgerufen werden, nachdem Sie alle Ihre Instanzvariablen initialisiert haben.

In Apples "Intermediate Swift" -Video (Sie finden es auf der Apple Developer-Video-Ressourcenseite https://developer.apple.com/videos/wwdc/2014/ ) wird gegen 28:40 Uhr ausdrücklich angegeben, dass alle Initialisierer in Die Superklasse muss aufgerufen werden, nachdem Sie Ihre Instanzvariablen initialisiert haben.

In Objective-C war es umgekehrt. Da in Swift alle Eigenschaften initialisiert werden müssen, bevor sie verwendet werden, müssen wir zuerst die Eigenschaften initialisieren. Dies soll verhindern, dass die "init ()" - Methode der Superklasse die aufgerufene Funktion aufruft, ohne zuvor die Eigenschaften zu initialisieren.

Die Implementierung von "Square" sollte also sein:

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength
        numberOfSides = 4
        super.init(name:name) // Correct position for "super.init()"
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}
Shuyang
quelle
1
Hätte nie gedacht. Initialisierung mit Super sollte Faustaussage sein, hätte ich gedacht. !! Hm. eine ziemliche Veränderung mit schnell.
mythicalcoder
Warum muss das danach kommen? Bitte geben Sie einen technischen Grund an
Masih
14

Entschuldigung für die hässliche Formatierung. Setzen Sie einfach ein Fragezeichen nach der Deklaration und alles wird in Ordnung sein. Eine Frage teilt dem Compiler mit, dass der Wert optional ist.

class Square: Shape {
    var sideLength: Double?   // <=== like this ..

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Edit1:

Es gibt eine bessere Möglichkeit, diesen Fehler zu überspringen. Laut dem Kommentar von jmaschad gibt es in Ihrem Fall keinen Grund, optional zu verwenden, da optionale Optionen nicht bequem zu verwenden sind und Sie immer überprüfen müssen, ob optional nicht null ist, bevor Sie darauf zugreifen. Alles was Sie tun müssen, ist das Mitglied nach der Deklaration zu initialisieren:

class Square: Shape {
    var sideLength: Double=Double()   

    init(sideLength:Double, name:String) {
        super.init(name:name)
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Edit2:

Nachdem zwei Minuten auf diese Antwort gekommen waren, fand ich einen noch besseren Weg. Wenn Sie möchten, dass das Klassenmitglied in Ihrem Konstruktor initialisiert wird, müssen Sie ihm im Konstruktor und vor dem Aufruf von super.init () einen Anfangswert zuweisen. So was:

class Square: Shape {
    var sideLength: Double  

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength   // <= before super.init call..
        super.init(name:name)
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Viel Glück beim Lernen von Swift.

fnc12
quelle
Einfach wechseln super.init(name:name)und self.sideLength = sideLength. Das Deklarieren sideLengthals optional ist falsch und führt später zu zusätzlichen Problemen, wenn Sie das Auspacken erzwingen müssen.
Johannes Luong
Ja, das ist eine Option. Danke
fnc12
Sie können tatsächlich nur haben var sideLength: Double, keine Notwendigkeit, ihm einen Anfangswert zuzuweisen
Jarsen
Was ist, wenn ich wirklich eine optionale Konstante habe? Was mache ich damit? Muss ich im Konstruktor initialisieren? Ich verstehe nicht, warum Sie es tun müssen, aber der Compiler beschwert sich in Swift 1.2
Van Du Tran
1
Perfekt ! Alle 3 Lösungen funktionierten "?", "String ()", aber das Problem für mich war, dass ich keine der Eigenschaften "zugewiesen" hatte, und als ich das tat, funktionierte es! Danke Kumpel
Naishta
9

swift erzwingt, dass Sie jede Mitgliedsvariable initialisieren, bevor sie jemals verwendet wird / werden könnte. Da es nicht sicher sein kann, was passiert, wenn es an der Reihe ist, tritt ein Fehler auf: Besser sicher als leid

Daij-Djan
quelle
1
Dies ist IMO nicht sinnvoll, da die übergeordnete Klasse keinen Einblick in die in ihren untergeordneten Elementen deklarierten Eigenschaften haben sollte!
Andy Hin
1
Es tut es nicht, aber Sie können
Dinge
Kannst du hier und die folgenden Kommentare sehen? Ich denke, ich sage genau das, was Sie sagen, dh wir sagen beide, dass der Compiler eher sicher als traurig sein möchte. Meine einzige Frage ist also, wie hat Objective-C dieses Problem gelöst? Oder nicht? Wenn nicht, warum müssen Sie dann immer noch super.initin die erste Zeile schreiben ?
Honig
7

Edward,

Sie können den Code in Ihrem Beispiel folgendermaßen ändern:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}

Dies verwendet eine implizit entpackte Option.

In der Dokumentation können wir lesen:

"Wenn Sie wie bei den Optionen keinen Anfangswert angeben, wenn Sie eine implizit entpackte optionale Variable oder Eigenschaft deklarieren, wird der Wert automatisch auf Null gesetzt."

Pavel Gubarev
quelle
Ich denke, das ist die sauberste Option. Bei meinem ersten Swift-Versuch hatte ich eine Mitgliedsvariable vom Typ AVCaptureDevice, die nicht direkt instanziiert werden kann, sodass init () - Code erforderlich war. ViewController erfordert jedoch mehrere Initialisierer, und Sie können keine gemeinsame Initialisierungsmethode von init () aus aufrufen. Daher scheint diese Antwort die einzige Option zu sein, die das Kopieren / Einfügen von doppeltem Code in jeden Initialisierer vermeidet.
Sunetos
6

Mit Swift können Sie keine Superklasse initialisieren, ohne die Eigenschaften zu initialisieren, umgekehrt zu Obj C. Sie müssen also alle Eigenschaften initialisieren, bevor Sie "super.init" aufrufen.

Bitte gehen Sie zu http://blog.scottlogic.com/2014/11/20/swift-initialisation.html . Es gibt eine schöne Erklärung für Ihr Problem.

Jishnu Bala
quelle
6

Fügen Sie am Ende der Deklaration null hinzu.


// Must be nil or swift complains
var someProtocol:SomeProtocol? = nil

// Init the view
override init(frame: CGRect)
    super.init(frame: frame)
    ...

Dies funktionierte für meinen Fall, aber möglicherweise nicht für Ihren

Michael
quelle
Gut, während mit UIView mit UIViewController mit Protokoll
abdul sathar
1

Sie starten nur in der falschen Reihenfolge.

     class Shape2 {
        var numberOfSides = 0
        var name: String
        init(name:String) {
            self.name = name
        }
        func simpleDescription() -> String {
            return "A shape with \(numberOfSides) sides."
        }
    }

    class Square2: Shape2 {
        var sideLength: Double

        init(sideLength:Double, name:String) {

            self.sideLength = sideLength
            super.init(name:name) // It should be behind "self.sideLength = sideLength"
            numberOfSides = 4
        }
        func area () -> Double {
            return sideLength * sideLength
        }
    }
ylgwhyh
quelle
0

Ich werde wahrscheinlich einige Abstimmungen erhalten, aber um ehrlich zu sein, ist das Leben auf diese Weise einfacher:

class CSListServerData<ListItem: CSJsonData>: CSServerData {
    var key: String!
    var type: ListItem.Type!
    var property: CSJsonDataList<ListItem>!

    func construct(_ key: String, _ type: ListItem.Type) -> Self {
        self.key = key
        self.type = type
        property = CSJsonDataList(self, type, key)
        return self
    }

    func construct(_ type: ListItem.Type) { construct("list", type) }

    var list: [ListItem] { property.list }
}
Renetik
quelle
-4

Es ist ein erstaunlich dummes Design.

Betrachten Sie so etwas:

.
.
.
var playerShip:PlayerShip
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Dies ist ungültig, wie oben erwähnt. Aber so ist:

.
.
.
var playerShip:PlayerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Weil 'Selbst' nicht initialisiert wurde.

Ich hoffe aufrichtig, dass dieser Fehler bald behoben ist.

(Ja, ich weiß, ich könnte ein leeres Objekt erstellen und dann die Größe einstellen, aber das ist einfach dumm).

Edward Kenworthy
quelle
2
Dies ist kein Fehler, seine neue Programmierung ist grundlegend für OOPs.
Hitendra Solanki