Práctica recomendada para implementar un inicializador fallido en Swift


100

Con el siguiente código trato de definir una clase de modelo simple y su inicializador fallido, que toma un diccionario (json-) como parámetro. El inicializador debería regresar nilsi el nombre de usuario no está definido en el json original.

1. ¿Por qué no se compila el código? El mensaje de error dice:

Todas las propiedades almacenadas de una instancia de clase deben inicializarse antes de devolver nil de un inicializador.

Eso no tiene sentido. ¿Por qué debería inicializar esas propiedades cuando planeo regresar nil?

2. ¿Es mi enfoque el correcto o habría otras ideas o patrones comunes para lograr mi objetivo?

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

Tuve un problema similar, con el mío llegué a la conclusión de que se debe esperar cada valor del diccionario y, por lo tanto, fuerzo el desenvolver los valores. Si la propiedad no está allí, podré detectar el error. Además, agregué un canSetCalculablePropertiesparámetro booleano que permite a mi inicializador calcular propiedades que pueden o no pueden crearse sobre la marcha. Por ejemplo, si dateCreatedfalta una clave y puedo establecer la propiedad sobre la marcha porque el canSetCalculablePropertiesparámetro es verdadero, simplemente lo configuro en la fecha actual.
Adam Carter

Respuestas:


71

Actualización: del registro de cambios de Swift 2.2 (publicado el 21 de marzo de 2016):

Los inicializadores de clase designados declarados como fallidos o lanzando ahora pueden devolver nil o arrojar un error, respectivamente, antes de que el objeto se haya inicializado por completo.


Para Swift 2.1 y versiones anteriores:

Según la documentación de Apple (y el error de su compilador), una clase debe inicializar todas sus propiedades almacenadas antes de regresar nilde un inicializador que falla:

Sin embargo, para las clases, un inicializador que falla puede desencadenar una falla de inicialización solo después de que todas las propiedades almacenadas introducidas por esa clase se hayan establecido en un valor inicial y se haya llevado a cabo cualquier delegación de inicializador.

Nota: En realidad, funciona bien para estructuras y enumeraciones, pero no para clases.

La forma sugerida de manejar las propiedades almacenadas que no se pueden inicializar antes de que falle el inicializador es declararlas como opcionales implícitamente desempaquetadas.

Ejemplo de los documentos:

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

En el ejemplo anterior, la propiedad de nombre de la clase Producto se define como si tuviera un tipo de cadena opcional sin envolver implícitamente (¡Cadena!). Debido a que es de un tipo opcional, esto significa que la propiedad del nombre tiene un valor predeterminado de nil antes de que se le asigne un valor específico durante la inicialización. Este valor predeterminado de cero a su vez significa que todas las propiedades introducidas por la clase Producto tienen un valor inicial válido. Como resultado, el inicializador que falla para el Producto puede desencadenar un error de inicialización al comienzo del inicializador si se le pasa una cadena vacía, antes de asignar un valor específico a la propiedad del nombre dentro del inicializador.

En su caso, sin embargo, sólo tiene que definir userNamecomo un String!problema no se soluciona el error de compilación, ya que todavía tiene que preocuparse sobre la inicialización de las propiedades de su clase base, NSObject. Afortunadamente, con userNamedefinido como a String!, puede llamar super.init()antes return nilque usted, lo que iniciará su NSObjectclase base y solucionará el error de compilación.

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
Muchas gracias, no solo bien, sino también bien explicado
Kai Huppmann

9
en swift1.2, el ejemplo de los documentos produce un error "Todas las propiedades almacenadas de una instancia de clase deben inicializarse antes de devolver nil de un inicializador"
jeffrey

2
@jeffrey Eso es correcto, el ejemplo de la documentación ( Productclase) no puede desencadenar una falla de inicialización antes de asignar un valor específico, aunque los documentos dicen que sí. Los documentos no están sincronizados con la última versión de Swift. En su lugar, se recomienda hacerlo varpor ahora let. fuente: Chris Lattner .
Arjan

1
La documentación tiene este código un poco diferente: primero establece la propiedad y luego verifica si está presente. Consulte "Inicializadores fallidos para clases", "El lenguaje de programación Swift". `` `class Product {let name: String! init? (nombre: String) {self.name = name if name.isEmpty {return nil}}} `` `
Misha Karpenko

También leí esto en los documentos de Apple, pero no veo por qué sería necesario. Una falla significaría devolver cero de todos modos, ¿qué importa entonces si las propiedades se han inicializado?
Alper

132

Eso no tiene sentido. ¿Por qué debería inicializar esas propiedades cuando planeo devolver cero?

Según Chris Lattner, esto es un error. Esto es lo que dice:

Esta es una limitación de implementación en el compilador swift 1.1, documentada en las notas de la versión. Actualmente, el compilador no puede destruir clases parcialmente inicializadas en todos los casos, por lo que no permite la formación de una situación en la que debería hacerlo. Consideramos que esto es un error que se solucionará en futuras versiones, no una característica.

Fuente

EDITAR:

Swift ahora es de código abierto y, de acuerdo con este registro de cambios , ahora está arreglado en instantáneas de swift 2.2

Los inicializadores de clase designados declarados como fallidos o lanzando ahora pueden devolver nil o arrojar un error, respectivamente, antes de que el objeto se haya inicializado por completo.


2
Gracias por abordar mi punto de que la idea de inicializar propiedades que ya no serán de ninguna necesidad parece no muy razonable. Y +1 por compartir una fuente, lo que demuestra que Chris Lattner se siente como yo;).
Kai Huppmann

22
FYI: "De hecho. Esto todavía es algo que nos gustaría mejorar, pero no pasamos el corte para Swift 1.2". - Chris Lattner 10 de febrero de 2015
dreamlab

14
Para su información: en Swift 2.0 beta 2, esto sigue siendo un problema, y ​​también es un problema con un inicializador que arroja.
aranasaurus

7

Acepto que la respuesta de Mike S es la recomendación de Apple, pero no creo que sea la mejor práctica. El objetivo de un sistema de tipos fuerte es mover los errores de tiempo de ejecución al tiempo de compilación. Esta "solución" frustra ese propósito. En mi humilde opinión, sería mejor seguir adelante e inicializar el nombre de usuario ""y luego verificarlo después de super.init (). Si se permiten nombres de usuario en blanco, establezca una marca.

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

Gracias, pero no veo cómo la respuesta de Mike corrompe las ideas de los sistemas de tipos fuertes. Con todo, presenta la misma solución con la diferencia de que el valor inicial se establece en "" en lugar de cero. Además, el código se quita para usar "" como nombre de usuario (que puede parecer bastante académico, pero al menos es diferente de no estar configurado en el json / dictionary)
Kai Huppmann

2
Tras la revisión, veo que tiene razón, pero solo porque userName es una constante. Si fuera una variable, entonces la respuesta aceptada sería peor que la mía porque userName podría establecerse más tarde en nil.
Daniel T.

Me gusta esta respuesta. @KaiHuppmann, si desea permitir nombres de usuario vacíos, también puede tener un simple Bool needReturnNil. Si el valor no existe en el diccionario, establezca needReturnNil en verdadero y establezca userName en lo que sea. Después de super.init (), verifique needReturnNil y devuelva nil si es necesario.
Richard Venable

6

Otra forma de eludir la limitación es trabajar con funciones de clase para realizar la inicialización. Es posible que incluso desee mover esa función a una extensión:

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 se convertiría en:

if let user = User.fromDictionary(someDict) {

     // Party hard
}

1
Me gusta esto; Prefiero que los constructores sean transparentes sobre lo que quieren, y pasar un diccionario es muy opaco.
Ben Leggiero


1

Descubrí que esto se puede hacer en Swift 1.2

Hay algunas condiciones:

  • Las propiedades requeridas deben declararse como opcionales sin empaquetar implícitamente
  • Asigne un valor a sus propiedades requeridas exactamente una vez. Este valor puede ser nulo.
  • Luego llame a super.init () si su clase está heredando de otra clase.
  • Después de que se haya asignado un valor a todas sus propiedades requeridas, verifique si su valor es el esperado. Si no, devuelva nil.

Ejemplo:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

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

0

Un inicializador que falla para un tipo de valor (es decir, una estructura o enumeración) puede desencadenar un error de inicialización en cualquier punto dentro de la implementación del inicializador.

Sin embargo, para las clases, un inicializador que falla puede desencadenar una falla de inicialización solo después de que todas las propiedades almacenadas introducidas por esa clase se hayan establecido en un valor inicial y se haya llevado a cabo cualquier delegación de inicializador.

Extracto de: Apple Inc. “ El lenguaje de programación Swift. ”IBooks. https://itun.es/sg/jEUH0.l


0

Puede utilizar el inicio de conveniencia :

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)
    } 
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.