Best practice per implementare un inizializzatore non riuscito in Swift


100

Con il codice seguente cerco di definire una semplice classe del modello ed è un inizializzatore failable, che accetta un dizionario (json-) come parametro. L'inizializzatore dovrebbe restituire nilse il nome utente non è definito nel json originale.

1. Perché il codice non viene compilato? Il messaggio di errore dice:

Tutte le proprietà memorizzate di un'istanza di classe devono essere inizializzate prima di restituire nil da un inizializzatore.

Non ha senso. Perché dovrei inizializzare queste proprietà quando prevedo di tornare nil?

2. Il mio approccio è quello giusto o ci sarebbero altre idee o schemi comuni per raggiungere il mio obiettivo?

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

Ho avuto un problema simile, con il mio ho concluso che ogni valore del dizionario dovrebbe essere previsto e quindi costringo a scartare i valori. Se la proprietà non è lì, sarò in grado di catturare il bug. Inoltre, ho aggiunto un canSetCalculablePropertiesparametro booleano che consente al mio inizializzatore di calcolare proprietà che possono o non possono essere create al volo. Ad esempio, se dateCreatedmanca una chiave e posso impostare la proprietà al volo perché il canSetCalculablePropertiesparametro è vero, lo imposto semplicemente alla data corrente.
Adam Carter

Risposte:


71

Aggiornamento: dal registro modifiche di Swift 2.2 (rilasciato il 21 marzo 2016):

Gli inizializzatori di classe designati dichiarati come failable o throwing possono ora restituire zero o generare un errore, rispettivamente, prima che l'oggetto sia stato completamente inizializzato.


Per Swift 2.1 e versioni precedenti:

Secondo la documentazione di Apple (e l'errore del compilatore), una classe deve inizializzare tutte le sue proprietà memorizzate prima di tornare nilda un inizializzatore non riuscito:

Per le classi, tuttavia, un inizializzatore non riuscito può attivare un errore di inizializzazione solo dopo che tutte le proprietà memorizzate introdotte da quella classe sono state impostate su un valore iniziale e qualsiasi delega dell'inizializzatore ha avuto luogo.

Nota: in realtà funziona bene per strutture ed enumerazioni, ma non per le classi.

Il modo suggerito per gestire le proprietà archiviate che non possono essere inizializzate prima che l'inizializzatore fallisca è dichiararle come optionals implicitamente scartate.

Esempio dai documenti:

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

Nell'esempio precedente, la proprietà name della classe Product è definita come avente un tipo di stringa facoltativo non incluso nel wrapping (String!). Poiché è di un tipo facoltativo, ciò significa che la proprietà name ha un valore predefinito nil prima che le venga assegnato un valore specifico durante l'inizializzazione. Questo valore predefinito di nil a sua volta significa che tutte le proprietà introdotte dalla classe Product hanno un valore iniziale valido. Di conseguenza, l'inizializzatore non riuscito per Product può attivare un errore di inizializzazione all'inizio dell'inizializzatore se viene passata una stringa vuota, prima di assegnare un valore specifico alla proprietà name all'interno dell'inizializzatore.

Nel tuo caso, però, semplicemente definendo userNamecome String!non si risolve l'errore di compilazione perché hai ancora bisogno di preoccuparsi per inizializzare le proprietà sulla vostra classe di base, NSObject. Fortunatamente, con userNamedefinito come a String!, puoi effettivamente chiamare super.init()prima di te return nilche avvierà la tua NSObjectclasse base e correggerà l'errore di compilazione.

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

1
Grazie mille non solo giusto, ma anche ben spiegato
Kai Huppmann

9
in swift1.2, l'esempio dalla documentazione fa un errore "Tutte le proprietà memorizzate di un'istanza di classe devono essere inizializzate prima di restituire zero da un inizializzatore"
Jeffrey

2
@ jeffrey Esatto, l'esempio dalla documentazione ( Productclasse) non può attivare un errore di inizializzazione prima di assegnare un valore specifico, anche se i documenti dicono che può. I documenti non sono sincronizzati con l'ultima versione di Swift. Si consiglia invece di renderlo un varper ora let. fonte: Chris Lattner .
Arjan

1
La documentazione ha questa parte di codice leggermente diversa: prima si imposta la proprietà e poi si controlla se è presente. Vedere "Inizializzatori non riusciti per le classi", "Il linguaggio di programmazione Swift". `` `class Product {let name: String! init? (name: String) {self.name = name if name.isEmpty {return nil}}} `` ``
Misha Karpenko

Ho letto anche questo nei documenti di Apple ma non riesco a capire perché sarebbe necessario. Un fallimento significherebbe comunque restituire zero, cosa importa allora se le proprietà sono state inizializzate?
Alper

132

Non ha senso. Perché dovrei inizializzare quelle proprietà quando prevedo di restituire zero?

Secondo Chris Lattner questo è un bug. Ecco cosa dice:

Questa è una limitazione dell'implementazione nel compilatore swift 1.1, documentata nelle note di rilascio. Il compilatore non è attualmente in grado di distruggere le classi parzialmente inizializzate in tutti i casi, quindi non consente la formazione di una situazione in cui dovrebbe farlo. Consideriamo questo un bug da correggere nelle versioni future, non una funzionalità.

fonte

MODIFICARE:

Così swift è ora open source e secondo questo log delle modifiche ora è stato risolto in istantanee di swift 2.2

Gli inizializzatori di classe designati dichiarati come failable o throwing possono ora restituire zero o generare un errore, rispettivamente, prima che l'oggetto sia stato completamente inizializzato.


2
Grazie per aver affrontato il mio punto che l'idea di inizializzare proprietà che non saranno più necessarie non sembra molto ragionevole. E +1 per aver condiviso una fonte, il che dimostra che Chris Lattner si sente come me;).
Kai Huppmann

22
FYI: "Effettivamente. Questo è ancora qualcosa che vorremmo migliorare, ma non è stato tagliato per Swift 1.2". - Chris Lattner 10 febbraio 2015
dreamlab

14
FYI: In Swift 2.0 beta 2 questo è ancora un problema, ed è anche un problema con un inizializzatore che genera.
aranasaurus

7

Accetto che la risposta di Mike S sia la raccomandazione di Apple, ma non credo che sia la migliore pratica. Il punto centrale di un sistema di tipi forte è spostare gli errori di runtime in tempo di compilazione. Questa "soluzione" sconfigge tale scopo. IMHO, meglio sarebbe andare avanti e inizializzare il nome utente su ""e poi controllarlo dopo super.init (). Se sono consentiti userNames vuoti, impostare un 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
        }
    }
}

Grazie, ma non vedo come le idee di sistemi di tipo forte siano corrotte dalla risposta di Mike. Tutto sommato si presenta la stessa soluzione con la differenza che il valore iniziale è posto a "" invece che a zero. Inoltre, il codice toglie l'uso di "" come nome utente (che potrebbe sembrare abbastanza accademico, ma almeno è diverso dall'essere non impostato nel json / dizionario)
Kai Huppmann

2
Dopo la revisione, vedo che hai ragione, ma solo perché userName è una costante. Se fosse una variabile, la risposta accettata sarebbe peggiore della mia perché userName potrebbe essere successivamente impostato su zero.
Daniel T.

Mi piace questa risposta. @KaiHuppmann, se vuoi consentire nomi utente vuoti, potresti anche avere un semplice Bool needsReturnNil. Se il valore non esiste nel dizionario, imposta needsReturnNil su true e imposta userName su qualsiasi cosa. Dopo super.init (), controlla needsReturnNil e restituisci nil se necessario.
Richard Venable

6

Un altro modo per aggirare la limitazione è lavorare con funzioni di classe per eseguire l'inizializzazione. Potresti anche voler spostare quella funzione in un'estensione:

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

Usarlo diventerebbe:

if let user = User.fromDictionary(someDict) {

     // Party hard
}

1
Mi piace questo; Preferisco che i costruttori siano trasparenti su ciò che vogliono e il passaggio in un dizionario è molto opaco.
Ben Leggiero


1

Ho scoperto che questo può essere fatto in Swift 1.2

Ci sono alcune condizioni:

  • Le proprietà richieste dovrebbero essere dichiarate come optionals implicitamente scartate
  • Assegna un valore alle proprietà richieste esattamente una volta. Questo valore può essere nullo.
  • Quindi chiama super.init () se la tua classe eredita da un'altra classe.
  • Dopo che a tutte le proprietà richieste è stato assegnato un valore, controlla se il loro valore è quello previsto. In caso contrario, restituisci zero.

Esempio:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

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

0

Un inizializzatore non riuscito per un tipo di valore (ovvero una struttura o enumerazione) può attivare un errore di inizializzazione in qualsiasi punto all'interno della sua implementazione dell'inizializzatore

Per le classi, tuttavia, un inizializzatore non riuscito può attivare un errore di inizializzazione solo dopo che tutte le proprietà memorizzate introdotte da quella classe sono state impostate su un valore iniziale e qualsiasi delega dell'inizializzatore ha avuto luogo.

Estratto da: Apple Inc. “ The Swift Programming Language. "IBooks. https://itun.es/sg/jEUH0.l


0

Puoi usare convenienza 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)
    } 
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.