Meilleures pratiques pour implémenter un initialiseur disponible dans Swift

100

Avec le code suivant, j'essaye de définir une classe de modèle simple et c'est un initialiseur disponible, qui prend un dictionnaire (json-) comme paramètre. L'initialiseur doit retourner nilsi le nom d'utilisateur n'est pas défini dans le json d'origine.

1. Pourquoi le code ne se compile-t-il pas? Le message d'erreur dit:

Toutes les propriétés stockées d'une instance de classe doivent être initialisées avant de renvoyer nil à partir d'un initialiseur.

Cela n'a pas de sens. Pourquoi devrais-je initialiser ces propriétés lorsque je prévois de revenir nil?

2. Mon approche est-elle la bonne ou y aurait-il d'autres idées ou modèles communs pour atteindre mon objectif?

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
la source
J'ai eu un problème similaire, avec le mien, j'ai conclu que chaque valeur de dictionnaire devait être attendue et je force donc le déballage des valeurs. Si la propriété n'est pas là, je serai en mesure d'attraper le bogue. De plus, j'ai ajouté un canSetCalculablePropertiesparamètre booléen permettant à mon initialiseur de calculer des propriétés qui peuvent ou ne peuvent pas être créées à la volée. Par exemple, si une dateCreatedclé est manquante et que je peux définir la propriété à la volée parce que le canSetCalculablePropertiesparamètre est vrai, je la mets simplement à la date actuelle.
Adam Carter

Réponses:

71

Mise à jour: à partir du journal des modifications de Swift 2.2 (publié le 21 mars 2016):

Les initialiseurs de classe désignés déclarés comme disponibles ou lancés peuvent désormais renvoyer nil ou générer une erreur, respectivement, avant que l'objet n'ait été complètement initialisé.


Pour Swift 2.1 et versions antérieures:

Selon la documentation d'Apple (et l'erreur de votre compilateur), une classe doit initialiser toutes ses propriétés stockées avant de revenir nildepuis un initialiseur disponible:

Pour les classes, cependant, un initialiseur disponible peut déclencher un échec d'initialisation uniquement après que toutes les propriétés stockées introduites par cette classe ont été définies sur une valeur initiale et que toute délégation d'initialisation a eu lieu.

Remarque: cela fonctionne bien pour les structures et les énumérations, mais pas pour les classes.

La méthode suggérée pour gérer les propriétés stockées qui ne peuvent pas être initialisées avant l'échec de l'initialiseur est de les déclarer comme des options implicitement déballées.

Exemple tiré de la documentation:

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

Dans l'exemple ci-dessus, la propriété name de la classe Product est définie comme ayant un type de chaîne facultatif implicitement déroulé (String!). Etant donné qu'elle est de type facultatif, cela signifie que la propriété name a une valeur par défaut de nil avant de recevoir une valeur spécifique lors de l'initialisation. Cette valeur par défaut de nil signifie à son tour que toutes les propriétés introduites par la classe Product ont une valeur initiale valide. Par conséquent, l'initialiseur disponible pour Product peut déclencher un échec d'initialisation au début de l'initialiseur s'il reçoit une chaîne vide, avant d'affecter une valeur spécifique à la propriété name dans l'initialiseur.

Dans votre cas, cependant, définir simplement userNamecomme String!ne résout pas l'erreur de compilation parce que vous avez encore à vous soucier de l' initialisation des propriétés de votre classe de base, NSObject. Heureusement, avec userNamedéfini comme a String!, vous pouvez réellement appeler super.init()avant vous, return nilce qui initiera votre NSObjectclasse de base et corrigera l'erreur de compilation.

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
la source
1
Merci beaucoup non seulement à droite, mais aussi bien expliqué
Kai Huppmann
9
dans swift1.2, l'exemple de la documentation fait une erreur "Toutes les propriétés stockées d'une instance de classe doivent être initialisées avant de renvoyer nil d'un initialiseur"
jeffrey
2
@jeffrey C'est correct, l'exemple de la documentation ( Productclasse) ne peut pas déclencher un échec d'initialisation avant d'attribuer une valeur spécifique, même si la documentation le dit. Les documents ne sont pas synchronisés avec la dernière version de Swift. Il est conseillé d'en faire un varpour l'instant à la place let. source: Chris Lattner .
Arjan
1
La documentation a ce morceau de code un peu différent: vous définissez d'abord la propriété, puis vérifiez si elle est présente. Voir «Initialiseurs disponibles pour les classes», «Le langage de programmation Swift». `` `` class Product {let name: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} `` ``
Misha Karpenko
J'ai lu cela aussi dans la documentation Apple, mais je ne vois pas pourquoi cela serait nécessaire. Un échec signifierait de toute façon retourner nil, qu'importe alors si les propriétés ont été initialisées?
Alper le
132

Cela n'a pas de sens. Pourquoi devrais-je initialiser ces propriétés alors que je prévois de renvoyer nil?

Selon Chris Lattner, c'est un bug. Voici ce qu'il dit:

Il s'agit d'une limitation d'implémentation dans le compilateur swift 1.1, documentée dans les notes de publication. Le compilateur est actuellement incapable de détruire les classes partiellement initialisées dans tous les cas, donc il interdit la formation d'une situation où il devrait le faire. Nous considérons cela comme un bogue à corriger dans les prochaines versions, pas comme une fonctionnalité.

La source

ÉDITER:

Donc, swift est maintenant open source et selon ce journal des modifications, il est maintenant corrigé dans les instantanés de swift 2.2

Les initialiseurs de classe désignés déclarés comme disponibles ou lancés peuvent désormais renvoyer nil ou générer une erreur, respectivement, avant que l'objet n'ait été complètement initialisé.

Mustafa
la source
2
Merci d'avoir répondu à mon point que l'idée d'initialiser des propriétés qui ne seront plus nécessaires ne semble pas très raisonnable. Et +1 pour partager une source, ce qui prouve que Chris Lattner se sent comme moi;).
Kai Huppmann
22
FYI: "En effet. C'est toujours quelque chose que nous aimerions améliorer, mais nous n'avons pas fait la coupe pour Swift 1.2". - Chris Lattner 10 février 2015
dreamlab
14
FYI: Dans Swift 2.0 beta 2, c'est toujours un problème, et c'est aussi un problème avec un initialiseur qui lève.
aranasaurus
7

J'accepte que la réponse de Mike S soit la recommandation d'Apple, mais je ne pense pas que ce soit la meilleure pratique. L'intérêt d'un système de type fort est de déplacer les erreurs d'exécution vers la compilation. Cette «solution» va à l'encontre de cet objectif. IMHO, mieux serait d'aller de l'avant et d'initialiser le nom d'utilisateur "", puis de le vérifier après le super.init (). Si des noms d'utilisateur vides sont autorisés, définissez un indicateur.

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.
la source
Merci, mais je ne vois pas comment les idées de systèmes de types forts sont corrompues par la réponse de Mike. Dans l'ensemble, vous présentez la même solution à la différence que la valeur initiale est définie sur "" au lieu de zéro. De plus, vous codez pour utiliser "" comme nom d'utilisateur (ce qui peut sembler assez académique, mais au moins c'est différent de ne pas être défini dans le json / dictionary)
Kai Huppmann
2
Après examen, je vois que vous avez raison, mais uniquement parce que userName est une constante. S'il s'agissait d'une variable, alors la réponse acceptée serait pire que la mienne car userName pourrait plus tard être mis à nil.
Daniel T.
J'aime cette réponse. @KaiHuppmann, si vous souhaitez autoriser les noms d'utilisateurs vides, vous pouvez également simplement avoir un simple Bool needsReturnNil. Si la valeur n'existe pas dans le dictionnaire, définissez needsReturnNil sur true et définissez userName sur n'importe quoi. Après super.init (), vérifiez needsReturnNil et renvoyez nil si nécessaire.
Richard Venable
6

Une autre façon de contourner la limitation est de travailler avec une classe-fonctions pour faire l'initialisation. Vous pourriez même vouloir déplacer cette fonction vers une extension:

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

L'utiliser deviendrait:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Kevin R
la source
1
J'aime ça; Je préfère que les constructeurs soient transparents sur ce qu'ils veulent, et passer dans un dictionnaire est très opaque.
Ben Leggiero
3

Bien que Swift 2.2 ait été publié et que vous n'ayez plus à initialiser complètement l'objet avant d'échouer l'initialiseur, vous devez tenir vos chevaux jusqu'à ce que https://bugs.swift.org/browse/SR-704 soit corrigé.

sssilver
la source
1

J'ai découvert que cela pouvait être fait dans Swift 1.2

Il y a quelques conditions:

  • Les propriétés requises doivent être déclarées comme des options implicitement déroulées
  • Attribuez une valeur à vos propriétés requises une seule fois. Cette valeur peut être nulle.
  • Appelez ensuite super.init () si votre classe hérite d'une autre classe.
  • Une fois que toutes vos propriétés requises ont reçu une valeur, vérifiez si leur valeur est celle attendue. Sinon, renvoyez nul.

Exemple:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Pim
la source
0

Un initialiseur disponible pour un type valeur (c'est-à-dire une structure ou une énumération) peut déclencher un échec d'initialisation à tout moment dans son implémentation d'initialisation

Pour les classes, cependant, un initialiseur disponible peut déclencher un échec d'initialisation uniquement après que toutes les propriétés stockées introduites par cette classe ont été définies sur une valeur initiale et que toute délégation d'initialisation a eu lieu.

Extrait de: Apple Inc. « Le langage de programmation Swift. «IBooks. https://itun.es/sg/jEUH0.l

utilisateur1046037
la source
0

Vous pouvez utiliser l' init de commodité :

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)
    } 
}
Максим Петров
la source