Error en la clase Swift: propiedad no inicializada en la llamada super.init

218

Tengo dos clases ShapeySquare

class Shape {
    var numberOfSides = 0
    var name: String
    init(name:String) {
        self.name = name
    }
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Con la implementación anterior obtengo el error:

property 'self.sideLength' not initialized at super.init call
    super.init(name:name)

¿Por qué tengo que configurar self.sideLengthantes de llamar super.init?

JuJoDi
fuente
Estoy bastante seguro de que esto tiene que ver con buenas prácticas de programación, no una restricción técnica real. Si Shape llamara a una función que Square ha anulado, Square podría querer usar sideLength, pero aún no se ha inicializado. Swift probablemente solo te restringe de hacer esto por accidente al obligarte a inicializar tus instancias antes de llamar a la clase base.
cwharris
3
Los ejemplos en el libro son malos. Se supone que siempre debe llamar a super.init () último para asegurarse de que todas las propiedades se hayan inicializado, como se explica a continuación.
Pascal

Respuestas:

173

Cita del lenguaje de programación Swift, que responde a su pregunta:

"El compilador de Swift realiza cuatro comprobaciones de seguridad útiles para asegurarse de que la inicialización de dos fases se complete sin error:"

Comprobación de seguridad 1 "Un inicializador designado debe garantizar que todas las" propiedades introducidas por su clase se inicialicen antes de delegar en un inicializador de superclase ".

Extracto de: Apple Inc. "El lenguaje de programación Swift". iBooks https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

Rubén
fuente
47
Ahora que es un cambio asombroso en comparación con C ++, C # o Java.
MDJ
12
@MDJ Claro que sí. Honestamente, tampoco veo el valor agregado.
Ruben
20
En realidad parece haber uno. Digamos en C #, el constructor de superclase no debería llamar a ningún método reemplazable (virtual), porque nadie sabe cómo reaccionarían con una subclase no completamente inicializada. En Swift está bien, ya que el estado subclass-extra está bien, cuando se está ejecutando el superclass constructor. Además, en Swift todos los métodos, excepto los finales, son reemplazables.
MDJ
17
En particular, me resulta molesto porque significa que, si quiero hacer una subclase de UIView que cree sus propias subvistas, tengo que inicializar esas subvistas sin marcos para comenzar y agregar los marcos más tarde ya que no puedo referirme a la vista limita hasta DESPUÉS de llamar a super.init.
Ceniza
55
@Janos si hace que la propiedad sea opcional, no tiene que inicializarla init.
JeremyP
105

Swift tiene una secuencia muy clara y específica de operaciones que se realizan en inicializadores. Comencemos con algunos ejemplos básicos y avancemos hasta llegar a un caso general.

Tomemos un objeto A. Lo definiremos de la siguiente manera.

class A {
    var x: Int
    init(x: Int) {
        self.x = x
    }
}

Observe que A no tiene una superclase, por lo que no puede llamar a una función super.init () ya que no existe.

Bien, ahora subclasemos A con una nueva clase llamada B.

class B: A {
    var y: Int
    init(x: Int, y: Int) {
        self.y = y
        super.init(x: x)
    }
}

Esta es una desviación de Objective-C donde [super init]normalmente se llamaría primero antes que cualquier otra cosa. No es así en Swift. Usted es responsable de garantizar que sus variables de instancia estén en un estado coherente antes de hacer cualquier otra cosa, incluidos los métodos de llamada (que incluye el inicializador de su superclase).

Hitendra Solanki
fuente
1
Esto es extremadamente útil, un claro ejemplo. ¡gracias!
FullMetalFist
¿Qué sucede si necesito el valor para calcular y, por ejemplo: init (y: Int) {self.y = y * self.x super.init ()}
6rod9
1
use algo como init (y: Int, x: Int = 0) {self.y = y * x; self.x = x; super.init (x: x)}, tampoco puede llamar directamente al constructor vacío para la superclase con referencia al ejemplo anterior, porque los nombres de la
superclase
43

De los documentos

Control de seguridad 1

Un inicializador designado debe asegurarse de que todas las propiedades introducidas por su clase se inicialicen antes de delegar en un inicializador de superclase.


¿Por qué necesitamos un control de seguridad como este?

Para responder esto, avancemos rápidamente el proceso de inicialización.

Inicialización de dos fases

La inicialización de clases en Swift es un proceso de dos fases. En la primera fase, a cada propiedad almacenada se le asigna un valor inicial por la clase que la introdujo. Una vez que se ha determinado el estado inicial de cada propiedad almacenada, comienza la segunda fase, y cada clase tiene la oportunidad de personalizar aún más sus propiedades almacenadas antes de que la nueva instancia se considere lista para usar.

El uso de un proceso de inicialización de dos fases hace que la inicialización sea segura, a la vez que brinda flexibilidad total a cada clase en una jerarquía de clases. La inicialización en dos fases evita que se acceda a los valores de las propiedades antes de que se inicialicen , y evita que los valores de las propiedades sean configurados inesperadamente por otro inicializador.

Entonces, para asegurarse de que el proceso de inicialización de dos pasos se realice como se definió anteriormente, hay cuatro controles de seguridad, uno de ellos es,

Control de seguridad 1

Un inicializador designado debe asegurarse de que todas las propiedades introducidas por su clase se inicialicen antes de delegar en un inicializador de superclase.

Ahora, la inicialización de dos fases nunca habla del orden, pero este control de seguridad, se introduce super.initpara ser ordenado, después de la inicialización de todas las propiedades.

La comprobación de seguridad 1 puede parecer irrelevante ya que, la inicialización de dos fases evita que se pueda acceder a los valores de propiedad antes de que puedan inicializarse , sin esta comprobación de seguridad 1.

Como en esta muestra

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        super.init(sides: 3, named: "Triangle") 
        self.hypotenuse = hypotenuse
    }
}

Triangle.initha inicializado, cada propiedad antes de ser utilizada. Entonces el control de seguridad 1 parece irrelevante,

Pero entonces podría haber otro escenario, un poco complejo,

class Shape {
    var name: String
    var sides : Int
    init(sides:Int, named: String) {
        self.sides = sides
        self.name = named
        printShapeDescription()
    }
    func printShapeDescription() {
        print("Shape Name :\(self.name)")
        print("Sides :\(self.sides)")
    }
}

class Triangle: Shape {
    var hypotenuse: Int
    init(hypotenuse:Int) {
        self.hypotenuse = hypotenuse
        super.init(sides: 3, named: "Triangle")
    }

    override func printShapeDescription() {
        super.printShapeDescription()
        print("Hypotenuse :\(self.hypotenuse)")
    }
}

let triangle = Triangle(hypotenuse: 12)

Salida:

Shape Name :Triangle
Sides :3
Hypotenuse :12

Aquí, si hubiéramos llamado al super.initantes de establecer el hypotenuse, la super.initllamada habría llamado al printShapeDescription()y, dado que se ha anulado, primero recurriría a la implementación de la clase Triangle printShapeDescription(). La printShapeDescription()clase Triangle accede a hypotenuseuna propiedad no opcional que aún no se ha inicializado. Y esto no está permitido ya que la inicialización en dos fases evita que se acceda a los valores de propiedad antes de que se inicialicen

Por lo tanto, asegúrese de que la inicialización de dos fases se realice según lo definido, debe haber un orden específico de llamada super.init, y es decir, después de inicializar todas las propiedades introducidas por la selfclase, por lo tanto, necesitamos un control de seguridad 1

BangOperator
fuente
1
Gran explicación, el por qué definitivamente debería agregarse a la respuesta principal.
Guy Daher
Entonces, básicamente, está diciendo, porque las superclases init pueden llamar a una función (anulada) ... en la que esa función accede a la propiedad de subclases, para evitar no tener valores establecidos, la llamada superdebe ocurrir después de que todos los valores estén establecidos. OK tiene sentido. ¿Se pregunta cómo lo hizo Objective-C entonces y por qué tuvo que llamar superprimero?
Miel
Esencialmente lo que estás señalando que es parecido a: colocación printShapeDescription() antes de self.sides = sides; self.name = named; lo que generaría este error: use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. El error del OP se da para reducir la 'posibilidad' de error de tiempo de ejecución.
Miel
Usé específicamente la palabra 'posibilidad', porque si printShapeDescriptionfuera una función a la que no se refería, selfes decir, sería algo como 'print ("nada"), entonces no habría problemas. (Sin embargo, incluso para eso, el compilador arrojaría un error, porque no es súper inteligente)
Miel
Bueno, objc simplemente no era seguro. Swift es de tipo seguro, por lo que los objetos que no son opcionales realmente deben ser distintos de cero
Daij-Djan
36

Se debe llamar a "super.init ()" después de inicializar todas las variables de instancia.

En el video "Intermediate Swift" de Apple (puede encontrarlo en la página de recursos de video para desarrolladores de Apple https://developer.apple.com/videos/wwdc/2014/ ), aproximadamente a las 28:40, se dice explícitamente que todos los inicializadores en la superclase debe llamarse DESPUÉS de inicializar sus variables de instancia.

En Objective-C, fue al revés. En Swift, dado que todas las propiedades deben inicializarse antes de usarse, primero debemos inicializar las propiedades. Esto está destinado a evitar una llamada a la función anulada del método "init ()" de la superclase, sin inicializar primero las propiedades.

Entonces la implementación de "Square" debería ser:

class Square: Shape {
    var sideLength: Double

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength
        numberOfSides = 4
        super.init(name:name) // Correct position for "super.init()"
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}
Shuyang
fuente
1
Nunca lo habría adivinado. La inicialización con super debería ser la primera declaración es lo que habría pensado. !! hm. Todo un cambio con Swift.
mythicalcoder
¿Por qué esto tiene que venir después? Proporcione una razón técnica
Masih
14

Perdón por el formato feo. Simplemente ponga un carácter de pregunta después de la declaración y todo estará bien. Una pregunta le dice al compilador que el valor es opcional.

class Square: Shape {
    var sideLength: Double?   // <=== like this ..

    init(sideLength:Double, name:String) {
        super.init(name:name) // Error here
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Editar1:

Hay una mejor manera de omitir este error. De acuerdo con el comentario de jmaschad, no hay ninguna razón para usar opcional en su caso porque las opcionales no son cómodas y siempre debe verificar si la opción no es nula antes de acceder a ella. Entonces, todo lo que tiene que hacer es inicializar el miembro después de la declaración:

class Square: Shape {
    var sideLength: Double=Double()   

    init(sideLength:Double, name:String) {
        super.init(name:name)
        self.sideLength = sideLength
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Edit2:

Después de dos inconvenientes en esta respuesta, encontré una forma aún mejor. Si desea que el miembro de la clase se inicialice en su constructor, debe asignarle un valor inicial dentro del contructor y antes de la llamada super.init (). Me gusta esto:

class Square: Shape {
    var sideLength: Double  

    init(sideLength:Double, name:String) {
        self.sideLength = sideLength   // <= before super.init call..
        super.init(name:name)
        numberOfSides = 4
    }
    func area () -> Double {
        return sideLength * sideLength
    }
}

Buena suerte en aprender Swift.

fnc12
fuente
Solo cambia super.init(name:name)y self.sideLength = sideLength. Declarar sideLengthque es opcional es erróneo e introduce problemas adicionales más adelante cuando tiene que forzar el desenvolvimiento.
Johannes Luong
Sí, esta es una opción. Gracias
fnc12
En realidad, puede tener var sideLength: Double, no es necesario asignarle un valor inicial
Jarsen
¿Qué pasa si realmente tengo una constante opcional? ¿Que hago con esto? ¿Tengo que inicializar en el constructor? No veo por qué tienes que hacerlo, pero el compilador se queja en Swift 1.2
Van Du Tran
1
Perfecto ! las 3 soluciones funcionaron "?", "String ()", pero el problema para mí fue que no había 'asignado' una de las propiedades, ¡y cuando lo hice funcionó! Gracias amigo
Naishta
9

swift le obliga a inicializar cada miembro var antes de que se use / pueda usarse. Dado que no puede estar seguro de lo que sucede cuando es super giro, se equivoca: más vale prevenir que curar

Daij-Djan
fuente
1
¡Esto no tiene sentido IMO porque la clase padre no debería tener visibilidad de las propiedades declaradas en sus hijos!
Andy Hin
1
No lo hace, pero puede anular las cosas y comenzar a usar self 'antes de que lo haga súper'
Daij-Djan
¿Puedes ver aquí y los comentarios que siguen? Creo que estoy diciendo exactamente lo que estás diciendo, es decir, los dos estamos diciendo que el compilador quiere estar seguro en lugar de disculparse, mi única pregunta es, ¿cómo resolvió este problema el objetivo-c? ¿O no fue así? Si no fue así, ¿por qué todavía requiere que escriba super.initen la primera línea?
Miel
7

Eduardo,

Puede modificar el código en su ejemplo así:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}

Esto está utilizando un opcional implícitamente sin envolver.

En la documentación podemos leer:

"Al igual que con los opcionales, si no proporciona un valor inicial cuando declara una variable o propiedad opcional implícitamente desenvuelta, su valor se establece automáticamente en nulo".

Pavel Gubarev
fuente
Creo que esta es la opción más limpia. En mi primer intento de Swift, tuve una variable miembro de tipo AVCaptureDevice, que no se puede instanciar directamente, por lo que se requería el código init (). Sin embargo, ViewController requiere múltiples inicializadores, y no puede llamar a un método de inicialización común desde init (), por lo que esta respuesta parece ser la única opción que evita copiar / pegar código duplicado en cada inicializador.
sunetos
6

Agregue nulo al final de la declaración.


// Must be nil or swift complains
var someProtocol:SomeProtocol? = nil

// Init the view
override init(frame: CGRect)
    super.init(frame: frame)
    ...

Esto funcionó para mi caso, pero puede no funcionar para el suyo

Miguel
fuente
Buena, mientras se usa con UIView con UIViewController con protocolo
abdul sathar
1

Simplemente estás iniciando en el orden incorrecto.

     class Shape2 {
        var numberOfSides = 0
        var name: String
        init(name:String) {
            self.name = name
        }
        func simpleDescription() -> String {
            return "A shape with \(numberOfSides) sides."
        }
    }

    class Square2: Shape2 {
        var sideLength: Double

        init(sideLength:Double, name:String) {

            self.sideLength = sideLength
            super.init(name:name) // It should be behind "self.sideLength = sideLength"
            numberOfSides = 4
        }
        func area () -> Double {
            return sideLength * sideLength
        }
    }
ylgwhyh
fuente
0

Probablemente recibiré algunos votos negativos, pero para ser sincero, la vida es más fácil de esta manera:

class CSListServerData<ListItem: CSJsonData>: CSServerData {
    var key: String!
    var type: ListItem.Type!
    var property: CSJsonDataList<ListItem>!

    func construct(_ key: String, _ type: ListItem.Type) -> Self {
        self.key = key
        self.type = type
        property = CSJsonDataList(self, type, key)
        return self
    }

    func construct(_ type: ListItem.Type) { construct("list", type) }

    var list: [ListItem] { property.list }
}
Renetik
fuente
-4

Es un diseño asombrosamente estúpido.

Considera algo como esto:

.
.
.
var playerShip:PlayerShip
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Esto no es válido, como se señaló anteriormente. Pero también lo es:

.
.
.
var playerShip:PlayerShip = PlayerShip(pos: CGPointMake(self.size.width / 2.0, 100))
var deltaPoint = CGPointZero

init(size: CGSize)
{
    super.init(size: size)
    playerLayerNode.addChild(playerShip)        
}
.
.
.

Porque 'yo' no se ha inicializado.

Sinceramente espero que este error se solucione pronto.

(Sí, sé que podría crear un objeto vacío y luego establecer el tamaño, pero eso es simplemente estúpido).

Edward Kenworthy
fuente
2
Esto no es un error, su nueva programación es fundamental para los OOP.
Hitendra Solanki