Best Practice zum Implementieren eines fehlerhaften Initialisierers in Swift

100

Mit dem folgenden Code versuche ich, eine einfache Modellklasse und ihren fehlgeschlagenen Initialisierer zu definieren, der ein (json-) Wörterbuch als Parameter verwendet. Der Initialisierer sollte zurückgeben, nilwenn der Benutzername im ursprünglichen JSON nicht definiert ist.

1. Warum wird der Code nicht kompiliert? Die Fehlermeldung lautet:

Alle gespeicherten Eigenschaften einer Klasseninstanz müssen initialisiert werden, bevor nil von einem Initialisierer zurückgegeben wird.

Das macht keinen Sinn. Warum sollte ich diese Eigenschaften initialisieren, wenn ich zurückkehren möchte nil?

2. Ist mein Ansatz der richtige oder gibt es andere Ideen oder gemeinsame Muster, um mein Ziel zu erreichen?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}
Kai Huppmann
quelle
Ich hatte ein ähnliches Problem, bei dem ich zu dem Schluss kam, dass jeder Wörterbuchwert zu erwarten ist, und erzwinge daher das Auspacken der Werte. Wenn die Eigenschaft nicht da ist, kann ich den Fehler fangen. Zusätzlich habe ich einen canSetCalculablePropertiesbooleschen Parameter hinzugefügt , mit dem mein Initialisierer Eigenschaften berechnen kann, die im laufenden Betrieb erstellt werden können oder nicht. Wenn beispielsweise ein dateCreatedSchlüssel fehlt und ich die Eigenschaft canSetCalculablePropertiesim laufenden Betrieb festlegen kann, weil der Parameter true ist, setze ich ihn einfach auf das aktuelle Datum.
Adam Carter

Antworten:

71

Update: Aus dem Swift 2.2-Änderungsprotokoll (veröffentlicht am 21. März 2016):

Bestimmte Klasseninitialisierer, die als fehlerhaft oder auslösend deklariert wurden, können jetzt null zurückgeben oder einen Fehler auslösen, bevor das Objekt vollständig initialisiert wurde.


Für Swift 2.1 und früher:

Gemäß der Dokumentation von Apple (und Ihrem Compilerfehler) muss eine Klasse alle gespeicherten Eigenschaften initialisieren, bevor sie nilvon einem fehlgeschlagenen Initialisierer zurückkehrt:

Bei Klassen kann ein fehlgeschlagener Initialisierer jedoch erst dann einen Initialisierungsfehler auslösen, wenn alle von dieser Klasse eingeführten gespeicherten Eigenschaften auf einen Anfangswert gesetzt wurden und eine Initialisierungsdelegierung stattgefunden hat.

Hinweis: Es funktioniert tatsächlich gut für Strukturen und Aufzählungen, nur nicht für Klassen.

Die vorgeschlagene Methode zum Behandeln gespeicherter Eigenschaften, die nicht initialisiert werden können, bevor der Initialisierer fehlschlägt, besteht darin, sie als implizit entpackte Optionen zu deklarieren.

Beispiel aus den Dokumenten:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

Im obigen Beispiel ist die Eigenschaft name der Produktklasse so definiert, dass sie einen implizit entpackten optionalen Zeichenfolgentyp (String!) Hat. Da es sich um einen optionalen Typ handelt, bedeutet dies, dass die Eigenschaft name den Standardwert nil hat, bevor ihr während der Initialisierung ein bestimmter Wert zugewiesen wird. Dieser Standardwert von nil bedeutet wiederum, dass alle von der Produktklasse eingeführten Eigenschaften einen gültigen Anfangswert haben. Infolgedessen kann der fehlgeschlagene Initialisierer für Product zu Beginn des Initialisierers einen Initialisierungsfehler auslösen, wenn ihm eine leere Zeichenfolge übergeben wird, bevor der Eigenschaft name im Initialisierer ein bestimmter Wert zugewiesen wird.

In Ihrem Fall wird der Kompilierungsfehler jedoch nicht durch einfaches Definieren userNameals String!behoben, da Sie sich weiterhin um die Initialisierung der Eigenschaften Ihrer Basisklasse kümmern müssen NSObject. Glücklicherweise können Sie mit userNamedefiniert als a String!tatsächlich super.init()vor Ihnen aufrufen , return nilwodurch Ihre NSObjectBasisklasse initiiert und der Kompilierungsfehler behoben wird.

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}
Mike S.
quelle
1
Vielen Dank nicht nur richtig, sondern auch gut erklärt
Kai Huppmann
9
in swift1.2, Beispiel aus den Dokumenten machen einen Fehler "Alle gespeicherten Eigenschaften einer Klasseninstanz müssen initialisiert werden, bevor Null von einem Initialisierer zurückgegeben wird"
Jeffrey
2
@jeffrey Das stimmt, das Beispiel aus der Dokumentation ( ProductKlasse) kann keinen Initialisierungsfehler auslösen, bevor ein bestimmter Wert zugewiesen wird, obwohl die Dokumente dies für möglich halten. Die Dokumente sind nicht mit der neuesten Swift-Version synchronisiert. Es wird empfohlen, es varstattdessen vorerst zu machen let. Quelle: Chris Lattner .
Arjan
1
In der Dokumentation ist dieser Code etwas anders: Sie legen zuerst die Eigenschaft fest und prüfen dann, ob sie vorhanden ist. Siehe "Verfügbare Initialisierer für Klassen", "Die schnelle Programmiersprache". `` `class Product {let name: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} `` `
Misha Karpenko
Ich habe dies auch in den Apple-Dokumenten gelesen, aber ich verstehe nicht, warum dies erforderlich wäre. Ein Fehler würde bedeuten, dass sowieso nichts zurückgegeben wird. Was macht es dann aus, ob die Eigenschaften initialisiert wurden?
Alper
132

Das macht keinen Sinn. Warum sollte ich diese Eigenschaften initialisieren, wenn ich null zurückgeben möchte?

Laut Chris Lattner ist dies ein Fehler. Folgendes sagt er:

Dies ist eine Implementierungsbeschränkung im Swift 1.1-Compiler, die in den Versionshinweisen dokumentiert ist. Der Compiler ist derzeit nicht in der Lage, teilweise initialisierte Klassen in allen Fällen zu zerstören, sodass die Bildung einer Situation, in der dies erforderlich wäre, nicht möglich ist. Wir betrachten dies als einen Fehler, der in zukünftigen Versionen behoben werden soll, nicht als eine Funktion.

Quelle

BEARBEITEN:

Swift ist jetzt Open Source und gemäß diesem Änderungsprotokoll ist es jetzt in Snapshots von Swift 2.2 behoben

Bestimmte Klasseninitialisierer, die als fehlerhaft oder auslösend deklariert wurden, können jetzt null zurückgeben oder einen Fehler auslösen, bevor das Objekt vollständig initialisiert wurde.

mustafa
quelle
2
Vielen Dank, dass Sie meinen Standpunkt angesprochen haben, dass die Idee, Eigenschaften zu initialisieren, die nicht mehr benötigt werden, nicht sehr vernünftig erscheint. Und +1 für das Teilen einer Quelle, was beweist, dass Chris Lattner sich wie ich fühlt;).
Kai Huppmann
22
Zu Ihrer Information: "In der Tat. Dies ist immer noch etwas, das wir verbessern möchten, aber den Schnitt für Swift 1.2 nicht geschafft haben." - Chris Lattner 10. Februar 2015
Dreamlab
14
Zu Ihrer Information: In Swift 2.0 Beta 2 ist dies immer noch ein Problem, und es ist auch ein Problem mit einem Initialisierer, der auslöst.
Aranasaurus
7

Ich akzeptiere, dass die Antwort von Mike S die Empfehlung von Apple ist, aber ich denke nicht, dass dies die beste Vorgehensweise ist. Der Sinn eines starken Typsystems besteht darin, Laufzeitfehler in die Kompilierungszeit zu verschieben. Diese "Lösung" macht diesen Zweck zunichte. IMHO wäre es besser, den Benutzernamen zu initialisieren ""und ihn dann nach der super.init () zu überprüfen. Wenn leere Benutzernamen zulässig sind, setzen Sie ein Flag.

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}
Daniel T.
quelle
Vielen Dank, aber ich sehe nicht, wie die Ideen starker Typsysteme durch Mikes Antwort verfälscht werden. Alles in allem präsentieren Sie dieselbe Lösung mit dem Unterschied, dass der Anfangswert auf "" anstatt auf Null gesetzt ist. Darüber hinaus nimmt Ihr Code weg, um "" als Benutzernamen zu verwenden (was recht akademisch erscheinen mag, aber zumindest anders ist, als nicht im json / dictionary festgelegt zu sein)
Kai Huppmann
2
Bei der Überprüfung sehe ich, dass Sie Recht haben, aber nur, weil userName eine Konstante ist. Wenn es eine Variable wäre, wäre die akzeptierte Antwort schlechter als meine, da userName später auf nil gesetzt werden könnte.
Daniel T.
Ich mag diese Antwort. @KaiHuppmann, wenn Sie leere Benutzernamen zulassen möchten, können Sie auch einfach einen Bool brauchtReturnNil haben. Wenn der Wert im Wörterbuch nicht vorhanden ist, setzen Sie needReturnNil auf true und userName auf was auch immer. Überprüfen Sie nach super.init () needReturnNil und geben Sie ggf. nil zurück.
Richard Venable
6

Eine andere Möglichkeit, die Einschränkung zu umgehen, besteht darin, mit einer Klassenfunktion zu arbeiten, um die Initialisierung durchzuführen. Vielleicht möchten Sie diese Funktion sogar in eine Erweiterung verschieben:

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

Die Verwendung würde werden:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Kevin R.
quelle
1
Ich mag das; Ich bevorzuge, dass Konstruktoren transparent darüber sind, was sie wollen, und die Übergabe eines Wörterbuchs ist sehr undurchsichtig.
Ben Leggiero
3

Obwohl Swift 2.2 veröffentlicht wurde und Sie das Objekt nicht mehr vollständig initialisieren müssen, bevor der Initialisierer fehlschlägt, müssen Sie Ihre Pferde halten, bis https://bugs.swift.org/browse/SR-704 behoben ist.

sssilver
quelle
1

Ich fand heraus , dies kann in Swift 1.2 erfolgen

Es gibt einige Bedingungen:

  • Erforderliche Eigenschaften sollten als implizit entpackte Optionen deklariert werden
  • Weisen Sie Ihren gewünschten Eigenschaften genau einmal einen Wert zu. Dieser Wert kann Null sein.
  • Rufen Sie dann super.init () auf, wenn Ihre Klasse von einer anderen Klasse erbt.
  • Nachdem allen erforderlichen Eigenschaften ein Wert zugewiesen wurde, überprüfen Sie, ob ihr Wert den Erwartungen entspricht. Wenn nicht, geben Sie nil zurück.

Beispiel:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Pim
quelle
0

Ein fehlgeschlagener Initialisierer für einen Werttyp (dh eine Struktur oder Aufzählung) kann an jedem Punkt seiner Initialisierungsimplementierung einen Initialisierungsfehler auslösen

Bei Klassen kann ein fehlgeschlagener Initialisierer jedoch erst dann einen Initialisierungsfehler auslösen, wenn alle von dieser Klasse eingeführten gespeicherten Eigenschaften auf einen Anfangswert gesetzt wurden und eine Initialisierungsdelegierung stattgefunden hat.

Auszug aus: Apple Inc. „ Die schnelle Programmiersprache. IBooks. https://itun.es/sg/jEUH0.l

user1046037
quelle
0

Sie können Convenience Init verwenden :

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

       self.init(userName: userName, isSuperUser: isSuperUser, someDetails: someDetails)
    } 
}
Максим Петров
quelle