web-dev-qa-db-fra.com

Il est recommandé d’implémenter un initialiseur disponible dans Swift

Avec le code suivant, j'essaie de définir une classe de modèle simple et son initializer failable, qui prend un dictionnaire (json) comme paramètre. L'initialiseur doit renvoyer nil si 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 retourner nil?

2. Est-ce que mon approche est 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()
    }
}
97
Kai Huppmann

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 étant disponibles ou renvoyés peuvent maintenant renvoyer une erreur 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 votre erreur de compilation), une classe doit initialiser toutes ses propriétés stockées avant de renvoyer nil à partir d'un initialiseur disponible:

Pour les classes, toutefois, un initialiseur non 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 qu'une délégation d'initialiseur ait eu lieu.

Remarque: Cela fonctionne très bien pour les structures et les énumérations, mais pas pour les classes.

La façon suggérée de gérer les propriétés stockées qui ne peuvent pas être initialisées avant l'échec de l'initialiseur consiste à les déclarer comme des options implicitement non enveloppé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 non enveloppé (String!). Comme il s'agit d'un type facultatif, cela signifie que la propriété name a la valeur par défaut nil avant qu'une valeur spécifique lui soit affectée lors de l'initialisation. Cette valeur par défaut de nil signifie que toutes les propriétés introduites par la classe Product ont une valeur initiale valide. En conséquence, l'initialiseur disponible pour Product peut déclencher un échec d'initialisation au début de l'initialiseur si une chaîne vide lui est transmise, avant d'affecter une valeur spécifique à la propriété name dans l'initialiseur.

Dans votre cas, toutefois, le simple fait de définir userName en tant que String! Ne corrige pas l'erreur de compilation, car vous devez toujours vous inquiéter de l'initialisation des propriétés de votre classe de base, NSObject. Heureusement, avec userName défini comme un String!, Vous pouvez appeler super.init() avant vous return nil Pour initialiser votre NSObject classe de base et corrigez 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
    }
}
69
Mike S

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

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

Il s'agit d'une limitation de l'implémentation du compilateur Swift 1.1, documentée dans les notes de version. Le compilateur ne peut actuellement pas détruire les classes partiellement initialisées dans tous les cas. Il est donc interdit de créer une situation Nous considérons qu’il s’agit d’un bogue à corriger dans les prochaines versions et non d’une fonctionnalité.

Source

EDIT:

Donc Swift est maintenant open source et selon ce changelog il est maintenant corrigé dans les instantanés de Swift 2.2

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

131
mustafa

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 au moment de 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 vérifiez-le après la super.init (). Si les 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
        }
    }
}
7
Daniel T.

Une autre façon de contourner la limitation est de travailler avec une fonction de classe pour effectuer l'initialisation. Vous voudrez peut-être même 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
}
6
Kevin R

Bien que Swift 2.2 ait été publié et que vous ne soyez plus obligé d'initialiser complètement l'objet avant l'échec de l'initialiseur, vous devez tenir vos chevaux jusqu'à https://bugs.Swift.org/browse/SR-704 est corrigé.

3
sssilver

J'ai découvert ceci peut être fait dans Swift 1.2

Il y a quelques conditions:

  • Les propriétés requises doivent être déclarées en tant qu'options implicitement non enveloppées
  • Attribuez une valeur aux 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.
  • Après toutes les propriétés requises ont reçu une valeur, vérifiez si leur valeur est celle attendue. Sinon, retourne zéro.

Exemple:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
1
Pim

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

Toutefois, pour les classes, un initialiseur disponible ne peut déclencher un échec d’initialisation que lorsque toutes les propriétés stockées introduites par cette classe ont été définies sur une valeur initiale et que la délégation de l’initialiseur a eu lieu.

Extrait de: Apple Inc. “ Le Swift Langage de programmation. ” iBooks . https://itun.es/sg/jEUH0.l

0
user1046037

Vous pouvez utiliser comfort init:

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