Warum wird in Swift überhaupt ein Convenience-Keyword benötigt?

132

Da Swift das Überladen von Methoden und Initialisierern unterstützt, können Sie mehrere initnebeneinander stellen und das verwenden, was Sie für zweckmäßig halten:

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    init() {
        self.name = "John"
    }
}

Warum sollte es überhaupt ein convenienceKeyword geben? Was macht das Folgende wesentlich besser?

class Person {
    var name:String

    init(name: String) {
        self.name = name
    }

    convenience init() {
        self.init(name: "John")
    }
}
Desmond Hume
quelle
13
Ich habe das gerade in der Dokumentation durchgelesen und war auch verwirrt. : /
Boidkan

Antworten:

235

Die vorhandenen Antworten erzählen nur die Hälfte der convenienceGeschichte. Die andere Hälfte der Geschichte, die Hälfte, die keine der vorhandenen Antworten abdeckt, beantwortet die Frage, die Desmond in den Kommentaren gestellt hat:

Warum sollte Swift mich zwingen, mich conveniencevor meinen Initialisierer zu stellen, nur weil ich von dort aus anrufen self.initmuss? "

Ich habe es in dieser Antwort , in der ich einige der Initialisierungsregeln von Swift ausführlich behandele, leicht angesprochen , aber das Hauptaugenmerk lag dort auf dem requiredWort. Aber diese Antwort sprach immer noch etwas an, das für diese Frage und diese Antwort relevant ist. Wir müssen verstehen, wie die Vererbung von Swift-Initialisierern funktioniert.

Da Swift keine nicht initialisierten Variablen zulässt, wird nicht garantiert, dass Sie alle (oder einige) Initialisierer von der Klasse erben, von der Sie erben. Wenn wir unserer Unterklasse Unterklassen hinzufügen und nicht initialisierte Instanzvariablen hinzufügen, haben wir aufgehört, Initialisierer zu erben. Und bis wir eigene Initialisierer hinzufügen, wird der Compiler uns anschreien.

Eine nicht initialisierte Instanzvariable ist eine Instanzvariable, der kein Standardwert zugewiesen wurde (wobei zu berücksichtigen ist, dass Optionals und implizit entpackte Optionals automatisch einen Standardwert von annehmen nil).

Also in diesem Fall:

class Foo {
    var a: Int
}

aist eine nicht initialisierte Instanzvariable. Dies wird nur kompiliert, wenn wir aeinen Standardwert angeben:

class Foo {
    var a: Int = 0
}

oder ain einer Initialisierungsmethode initialisieren:

class Foo {
    var a: Int

    init(a: Int) {
        self.a = a
    }
}

Nun wollen wir sehen, was passiert, wenn wir eine Unterklasse Foobilden.

class Bar: Foo {
    var b: Int

    init(a: Int, b: Int) {
        self.b = b
        super.init(a: a)
    }
}

Richtig? Wir haben eine Variable hinzugefügt und einen Initialisierer hinzugefügt, um einen Wert festzulegen, bdamit er kompiliert wird. Abhängig davon, aus welcher Sprache Sie kommen, können Sie erwarten, dass Barder FooInitialisierer geerbt wurde init(a: Int). Aber das tut es nicht. Und wie könnte es? Wie funktioniert Foo‚s init(a: Int)Know - how einen Wert an die zuweisen bVariable, die Barhinzugefügt? Das tut es nicht. Daher können wir eine BarInstanz nicht mit einem Initialisierer initialisieren, der nicht alle unsere Werte initialisieren kann.

Was hat das alles damit zu tun convenience?

Schauen wir uns die Regeln für die Initialisierungsvererbung an :

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 Superklassen-Komfortinitialisierer.

Beachten Sie Regel 2, in der Convenience-Initialisierer erwähnt werden.

Das convenienceSchlüsselwort gibt uns also an, welche Initialisierer von Unterklassen geerbt werden können, die Instanzvariablen ohne Standardwerte hinzufügen.

Nehmen wir diese Beispielklasse Base:

class Base {
    let a: Int
    let b: Int

    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }

    convenience init() {
        self.init(a: 0, b: 0)
    }

    convenience init(a: Int) {
        self.init(a: a, b: 0)
    }

    convenience init(b: Int) {
        self.init(a: 0, b: b)
    }
}

Beachten Sie, dass wir hier drei convenienceInitialisierer haben. Das heißt, wir haben drei Initialisierer, die vererbt werden können. Und wir haben einen bestimmten Initialisierer (ein bestimmter Initialisierer ist einfach ein beliebiger Initialisierer, der kein praktischer Initialisierer ist).

Wir können Instanzen der Basisklasse auf vier verschiedene Arten instanziieren:

Geben Sie hier die Bildbeschreibung ein

Erstellen wir also eine Unterklasse.

class NonInheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }
}

Wir erben von Base. Wir haben unsere eigene Instanzvariable hinzugefügt und keinen Standardwert angegeben, daher müssen wir unsere eigenen Initialisierer hinzufügen. Wir haben einen hinzugefügt init(a: Int, b: Int, c: Int), der jedoch nicht mit der Signatur des von der BaseKlasse festgelegten Initialisierers übereinstimmt : init(a: Int, b: Int). Das heißt, wir erben keine Initialisierer von Base:

Geben Sie hier die Bildbeschreibung ein

Was würde also passieren, wenn wir von erben würden Base, aber wir haben einen Initialisierer implementiert, der mit dem festgelegten Initialisierer von übereinstimmt Base?

class Inheritor: Base {
    let c: Int

    init(a: Int, b: Int, c: Int) {
        self.c = c
        super.init(a: a, b: b)
    }

    convenience override init(a: Int, b: Int) {
        self.init(a: a, b: b, c: 0)
    }
}

Zusätzlich zu den beiden Initialisierern, die wir direkt in dieser Klasse implementiert haben Base, können wir jetzt alle Initialisierer der BaseKlasse erben , da wir einen Initialisierer implementiert haben, der dem festgelegten Initialisierer der Klasse entspricht convenience:

Geben Sie hier die Bildbeschreibung ein

Die Tatsache, dass der Initialisierer mit der passenden Signatur als markiert convenienceist, spielt hier keine Rolle . Dies bedeutet nur, dass Inheritornur ein Initialisierer festgelegt ist. Wenn wir also von erben Inheritor, müssen wir nur diesen einen festgelegten Initialisierer implementieren, und dann erben wir den Inheritorpraktischen Initialisierer, was wiederum bedeutet, dass wir alle Basefestgelegten Initialisierer implementiert haben und seine convenienceInitialisierer erben können .

nhgrif
quelle
16
Die einzige Antwort, die die Frage tatsächlich beantwortet und den Dokumenten folgt. Ich würde es akzeptieren, wenn ich das OP wäre.
FreeNickname
12
Du solltest ein Buch schreiben;)
coolbeet
1
@SLN Diese Antwort behandelt viel darüber, wie die Vererbung von Swift-Initialisierern funktioniert.
nhgrif
1
@SLN Da eine Bar zu schaffen mit init(a: Int)verlassen würde bnicht initialisierten.
Ian Warburton
2
@ IanWarburton Ich kenne die Antwort auf dieses spezielle "Warum" nicht. Ihre Logik im zweiten Teil Ihres Kommentars scheint mir zutreffend zu sein, aber in der Dokumentation heißt es eindeutig, dass dies so funktioniert, und wenn Sie ein Beispiel für das, was Sie auf einem Spielplatz fragen, aufrufen, wird bestätigt, dass das Verhalten mit dem Dokumentierten übereinstimmt.
nhgrif
9

Meistens Klarheit. Aus Ihrem zweiten Beispiel:

init(name: String) {
    self.name = name
}

ist erforderlich oder bezeichnet . Es muss alle Ihre Konstanten und Variablen initialisieren. Komfortinitialisierer sind optional und können normalerweise verwendet werden, um die Initialisierung zu vereinfachen. Angenommen, Ihre Personenklasse hat ein optionales variables Geschlecht:

var gender: Gender?

wo Geschlecht eine Aufzählung ist

enum Gender {
  case Male, Female
}

Sie könnten Convenience-Initialisierer wie diesen haben

convenience init(maleWithName: String) {
   self.init(name: name)
   gender = .Male
}

convenience init(femaleWithName: String) {
   self.init(name: name)
   gender = .Female
}

Convenience-Initialisierer müssen die darin angegebenen oder erforderlichen Initialisierer aufrufen . Wenn Ihre Klasse eine Unterklasse ist, muss sie super.init() innerhalb ihrer Initialisierung aufgerufen werden.

Nate Mann
quelle
2
Für den Compiler wäre es also völlig offensichtlich, was ich mit mehreren Initialisierern auch ohne convenienceSchlüsselwort zu tun versuche, aber Swift würde immer noch Fehler machen. Das ist nicht die Art von Einfachheit, die ich von Apple erwartet hatte =)
Desmond Hume
2
Diese Antwort beantwortet nichts. Sie sagten "Klarheit", erklärten aber nicht, wie es etwas klarer macht.
Robo Robok
7

Nun, als erstes fällt mir ein, dass es bei der Klassenvererbung für die Codeorganisation und Lesbarkeit verwendet wird. Stellen Sie sich Personein solches Szenario vor, wenn Sie mit Ihrer Klasse fortfahren

class Person{
    var name: String
    init(name: String){
        self.name = name
    }

    convenience init(){
        self.init(name: "Unknown")
    }
}


class Employee: Person{
    var salary: Double
    init(name:String, salary:Double){
        self.salary = salary
        super.init(name: name)
    }

    override convenience init(name: String) {
        self.init(name:name, salary: 0)
    }
}

let employee1 = Employee() // {{name "Unknown"} salary 0}
let john = Employee(name: "John") // {{name "John"} salary 0}
let jane = Employee(name: "Jane", salary: 700) // {{name "Jane"} salary 700}

Mit dem praktischen Initialisierer kann ich ein Employee()Objekt ohne Wert erstellen , daher das Wortconvenience

u54r
quelle
2
convenienceWürde Swift mit weggenommenen Schlüsselwörtern nicht genug Informationen erhalten, um sich genauso zu verhalten?
Desmond Hume
Nein, wenn Sie das convenienceSchlüsselwort entfernen , können Sie das EmployeeObjekt nicht ohne Argument initialisieren .
U54r
Insbesondere Employee()ruft der Aufruf den (geerbten, aufgrund von convenience) Initialisierer auf init(), der aufruft self.init(name: "Unknown"). init(name: String), auch ein praktischer Initialisierer für Employee, ruft den angegebenen Initialisierer auf.
BallpointBen
1

Abgesehen von den Punkten, die andere Benutzer hier erklärt haben, ist mein Verständnis.

Ich spüre stark die Verbindung zwischen Convenience-Initialisierer und Erweiterungen. Convenience-Initialisierer sind für mich am nützlichsten, wenn ich die Initialisierung einer vorhandenen Klasse ändern (in den meisten Fällen kurz oder einfach machen) möchte.

Beispielsweise hat eine von Ihnen verwendete Klasse eines Drittanbieters initvier Parameter, aber in Ihrer Anwendung haben die letzten beiden den gleichen Wert. Um mehr Eingabe zu vermeiden und Ihren Code sauber zu machen, können Sie einen convenience initmit nur zwei Parametern definieren und darin self.initden Parameter last to to mit Standardwerten aufrufen .

Abdullah
quelle
1
Warum sollte Swift mich zwingen, conveniencevor meinen Initialisierer zu stellen, nur weil ich von dort aus anrufen self.initmuss? Dies scheint überflüssig und irgendwie unpraktisch.
Desmond Hume
1

Gemäß der Swift 2.1-Dokumentation müssen convenienceInitialisierer bestimmte Regeln einhalten:

  1. Ein convenienceInitialisierer kann nur Intializer in derselben Klasse aufrufen, nicht in Superklassen (nur über, nicht über)

  2. Ein convenienceInitialisierer muss irgendwo in der Kette einen bestimmten Initialisierer aufrufen

  3. Ein convenienceInitialisierer kann KEINE Eigenschaft ändern, bevor er einen anderen Initialisierer aufgerufen hat - während ein bestimmter Initialisierer Eigenschaften initialisieren muss, die von der aktuellen Klasse eingeführt werden, bevor ein anderer Initialisierer aufgerufen wird .

Durch die Verwendung des convenienceSchlüsselworts weiß der Swift-Compiler, dass er nach diesen Bedingungen suchen muss - sonst könnte er nicht.

Das Auge
quelle
Wahrscheinlich könnte der Compiler dies ohne das convenienceSchlüsselwort klären .
nhgrif
Darüber hinaus ist Ihr dritter Punkt irreführend. Ein Convenience-Initialisierer kann nur Eigenschaften ändern (und nicht ändernlet Eigenschaften ). Eigenschaften können nicht initialisiert werden. Ein designierter Initialisierer hat die Verantwortung, alle eingeführten Eigenschaften zu superinitialisieren, bevor er einen designierten Initialisierer aufruft .
nhgrif
1
Zumindest das Convenience-Schlüsselwort macht es dem Entwickler klar, und es kommt auch auf die Lesbarkeit an (plus die Überprüfung des Initialisierers anhand der Erwartungen des Entwicklers). Ihr zweiter Punkt ist gut, ich habe meine Antwort entsprechend geändert.
Auge
1

Eine Klasse kann mehr als einen festgelegten Initialisierer haben. Ein Convenience-Initialisierer ist ein sekundärer Initialisierer, der einen bestimmten Initialisierer derselben Klasse aufrufen muss.

Abdul Yasin
quelle