Erreur dans la classe Swift: propriété non initialisée lors de l'appel super.init

218

J'ai deux classes, ShapeetSquare

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

Avec l'implémentation ci-dessus, j'obtiens l'erreur:

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

Pourquoi dois-je régler self.sideLengthavant d'appeler super.init?

JuJoDi
la source
à peu près sûr que cela a à voir avec les bonnes pratiques de programmation, pas une restriction technique réelle. Si Shape devait appeler une fonction que Square a remplacée, Square pourrait vouloir utiliser sideLength, mais elle n'a pas encore été initialisée. Swift vous empêche probablement de le faire par accident en vous forçant à initialiser vos instances avant d'appeler la classe de base.
cwharris
3
Les exemples du livre sont mauvais. Vous êtes censé toujours appeler super.init () pour être sûr que toutes les propriétés ont été initialisées, comme expliqué ci-dessous.
Pascal

Réponses:

173

Citation du langage de programmation Swift, qui répond à votre question:

«Le compilateur de Swift effectue quatre vérifications de sécurité utiles pour s'assurer que l'initialisation en deux phases est terminée sans erreur:»

Contrôle de sécurité 1 «Un initialiseur désigné doit s'assurer que toutes les« propriétés introduites par sa classe sont initialisées avant de déléguer jusqu'à un initialiseur de superclasse ».

Extrait de: Apple Inc. «The Swift Programming Language». iBooks. https://itunes.apple.com/us/book/swift-programming-language/id881256329?mt=11

Ruben
la source
47
Maintenant, c'est un changement stupéfiant par rapport à C ++, C # ou Java.
MDJ
12
@MDJ Bien sûr. Honnêtement, je n'en vois pas non plus la valeur ajoutée.
Ruben
20
En fait, il semble y en avoir un. Disons qu'en C #, le constructeur de superclasse ne devrait appeler aucune méthode (virtuelle) remplaçable, car personne ne sait comment il réagirait avec une sous-classe non complètement initialisée. Dans Swift, c'est ok, car l'état de sous-classe supplémentaire est correct, lorsque le constructeur de super-classe est en cours d'exécution. De plus, dans Swift, toutes les méthodes, sauf les méthodes finales, peuvent être remplacées.
MDJ
17
Je le trouve particulièrement ennuyeux car cela signifie que si je veux créer une sous-classe UIView qui crée ses propres sous-vues, je dois d'abord initialiser ces sous-vues sans cadres et ajouter les images plus tard car je ne peux pas me référer à la vue. limites jusqu'à APRÈS avoir appelé super.init.
Ash
5
@Janos si vous rendez la propriété facultative, vous n'avez pas à l'initialiser init.
JeremyP
105

Swift a une séquence d'opérations très claire et spécifique qui se fait dans les initialiseurs. Commençons par quelques exemples de base et progressons jusqu'à un cas général.

Prenons un objet A. Nous le définirons comme suit.

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

Notez que A n'a pas de superclasse, il ne peut donc pas appeler une fonction super.init () car elle n'existe pas.

OK, alors maintenant, sous-classe A avec une nouvelle classe nommée B.

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

Il s'agit d'une dérogation à Objective-C, qui [super init]serait généralement appelée en premier avant toute autre chose. Ce n'est pas le cas à Swift. Vous êtes responsable de vous assurer que vos variables d'instance sont dans un état cohérent avant de faire quoi que ce soit d'autre, y compris les méthodes d'appel (qui inclut l'initialiseur de votre superclasse).

Hitendra Solanki
la source
1
c'est extrêmement utile, un exemple clair. Merci!
FullMetalFist
Et si j'ai besoin de la valeur pour calculer y, par exemple: init (y: Int) {self.y = y * self.x super.init ()}
6rod9
1
utilisez quelque chose comme, init (y: Int, x: Int = 0) {self.y = y * x; self.x = x; super.init (x: x)}, vous ne pouvez pas non plus appeler directement un constructeur vide pour la super classe en faisant référence à l'exemple ci-dessus, car les noms de super classes A n'ont pas de constructeur vide
Hitendra Solanki
43

À partir des documents

Contrôle de sécurité 1

Un initialiseur désigné doit s'assurer que toutes les propriétés introduites par sa classe sont initialisées avant de déléguer jusqu'à un initialiseur de superclasse.


Pourquoi avons-nous besoin d'un contrôle de sécurité comme celui-ci?

Pour répondre à cela, nous allons parcourir le processus d'initialisation en un rien de temps.

Initialisation en deux phases

L'initialisation de classe dans Swift est un processus en deux phases. Dans la première phase, chaque propriété stockée se voit attribuer une valeur initiale par la classe qui l'a introduite. Une fois que l'état initial de chaque propriété stockée a été déterminé, la deuxième phase commence et chaque classe a la possibilité de personnaliser davantage ses propriétés stockées avant que la nouvelle instance ne soit considérée comme prête à l'emploi.

L'utilisation d'un processus d'initialisation en deux phases rend l'initialisation sûre, tout en offrant une flexibilité complète à chaque classe dans une hiérarchie de classes. L'initialisation en deux phases empêche d'accéder aux valeurs de propriété avant qu'elles ne soient initialisées et empêche que les valeurs de propriété soient définies sur une valeur différente par un autre initialiseur de manière inattendue.

Donc, pour vous assurer que le processus d'initialisation en deux étapes est effectué comme défini ci-dessus, il y a quatre contrôles de sécurité, l'un d'eux est,

Contrôle de sécurité 1

Un initialiseur désigné doit s'assurer que toutes les propriétés introduites par sa classe sont initialisées avant de déléguer jusqu'à un initialiseur de superclasse.

Or, l'initialisation en deux phases ne parle jamais d'ordre, mais ce contrôle de sécurité, introduit super.inità ordonner, après l'initialisation de toutes les propriétés.

Le contrôle de sécurité 1 peut sembler non pertinent, car l' initialisation en deux phases empêche d'accéder aux valeurs des propriétés avant qu'elles ne soient initialisées , sans ce contrôle de sécurité 1.

Comme dans cet exemple

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.inita initialisé, chaque propriété avant d'être utilisée. Donc, le contrôle de sécurité 1 ne semble pas pertinent,

Mais il pourrait y avoir un autre scénario, un peu complexe,

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)

Production :

Shape Name :Triangle
Sides :3
Hypotenuse :12

Ici, si nous avions appelé super.initavant de définir le hypotenuse, l' super.initappel aurait alors appelé le printShapeDescription()et puisque cela a été remplacé, il reviendrait d'abord à l'implémentation de la classe Triangle printShapeDescription(). La printShapeDescription()classe of Triangle accède à hypotenuseune propriété non facultative qui n'a pas encore été initialisée. Et cela n'est pas autorisé car l' initialisation en deux phases empêche l'accès aux valeurs des propriétés avant leur initialisation

Assurez-vous donc que l'initialisation en deux phases se fait comme défini, il doit y avoir un ordre spécifique d'appel super.init, et c'est-à-dire, après avoir initialisé toutes les propriétés introduites par selfclasse, nous avons donc besoin d'un contrôle de sécurité 1

BangOperator
la source
1
Grande explication, le pourquoi devrait certainement être ajouté à la première réponse.
Guy Daher
Donc, vous dites essentiellement, parce que la superclasse init peut appeler une fonction (remplacée) ... dans laquelle cette fonction accède à la propriété des sous-classes, puis pour éviter de ne pas avoir de valeurs définies, l'appel à superdoit se produire après que toutes les valeurs sont définies. OK a du sens. Vous vous demandez comment Objective-C a fait alors et pourquoi vous avez dû appeler en superpremier?
Honey
Essentiellement , ce que vous pointez vers est similaire à: placer printShapeDescription() avant self.sides = sides; self.name = named; qui générerait cette erreur: use of 'self' in method call 'printShapeDescription' before all stored properties are initialized. L'erreur de l'OP est donnée pour réduire la «possibilité» d'erreur d'exécution.
Honey
J'ai spécifiquement utilisé le mot «possibilité», parce que si printShapeDescriptionc'était une fonction qui ne faisait pas référence, selfc'est- à- dire quelque chose comme «print» («rien»), il n'y aurait eu aucun problème. (Pourtant, même pour cela, le compilateur lancerait une erreur, car ce n'est pas super intelligent)
Honey
Eh bien, objc n'était tout simplement pas sûr. Swift est sûr de type donc les objets qui ne sont pas optionnels doivent vraiment être non-nil!
Daij-Djan
36

Le "super.init ()" doit être appelé après avoir initialisé toutes vos variables d'instance.

Dans la vidéo "Intermediate Swift" d'Apple (vous pouvez la trouver sur la page de ressources vidéo pour les développeurs Apple https://developer.apple.com/videos/wwdc/2014/ ), vers 28 h 40, il est dit explicitement que tous les initialiseurs de la super classe doit être appelée APRÈS avoir initialisé vos variables d'instance.

Dans Objective-C, c'était l'inverse. Dans Swift, puisque toutes les propriétés doivent être initialisées avant d'être utilisées, nous devons d'abord initialiser les propriétés. Ceci est destiné à empêcher un appel à une fonction redéfinie à partir de la méthode "init ()" de la super classe, sans initialiser les propriétés au préalable.

La mise en œuvre de "Square" devrait donc être:

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
la source
1
Je n'aurais jamais deviné. L'initialisation avec super devrait être la première déclaration, c'est ce que j'aurais pensé. !! hm. tout à fait un changement avec rapide.
mythicalcoder
Pourquoi cela doit venir après? Veuillez fournir une raison technique
Masih
14

Désolé pour le formatage moche. Mettez simplement un caractère de question après la déclaration et tout ira bien. Une question indique au compilateur que la valeur est facultative.

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

Edit1:

Il existe une meilleure façon d'ignorer cette erreur. Selon le commentaire de jmaschad, il n'y a aucune raison d'utiliser facultatif dans votre cas car les optionnels ne sont pas confortables à utiliser et vous devez toujours vérifier si facultatif n'est pas nul avant d'y accéder. Il vous suffit donc d'initialiser le membre après la déclaration:

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:

Après deux inconvénients à cette réponse, j'ai trouvé un moyen encore meilleur. Si vous voulez que le membre de la classe soit initialisé dans votre constructeur, vous devez lui attribuer une valeur initiale à l'intérieur de contructor et avant l'appel de super.init (). Comme ça:

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

Bonne chance pour apprendre Swift.

fnc12
la source
Il suffit de changer super.init(name:name)et self.sideLength = sideLength. Déclarer sideLengthcomme facultatif est erroné et introduit plus de tracas plus tard lorsque vous devez forcer le déballage.
Johannes Luong
Ouais, c'est une option. Merci
fnc12
En fait, vous pouvez simplement avoir var sideLength: Double, pas besoin de lui affecter une valeur initiale
Jarsen
Et si j'ai vraiment une constante optionnelle. Que dois-je en faire? Dois-je initialiser dans le constructeur? Je ne vois pas pourquoi vous devez le faire, mais le compilateur se plaint dans Swift 1.2
Van Du Tran
1
Parfait! les 3 solutions ont fonctionné "?", "String ()", mais le problème pour moi était que je n'avais pas "attribué" l'une des propriétés, et quand je l'ai fait, cela a fonctionné! Merci compagnon
Naishta
9

swift vous oblige à initialiser chaque var membre avant qu'il ne soit / ne soit jamais utilisé. Puisqu'il ne peut pas être sûr de ce qui se passe quand c'est le tour, il se trompe: mieux vaut prévenir que guérir

Daij-Djan
la source
1
Cela n'a pas de sens IMO car la classe parent ne devrait pas avoir de visibilité sur les propriétés déclarées dans ses enfants!
Andy Hin
1
Ce n'est pas le cas, mais vous pouvez remplacer les choses et commencer à vous utiliser `` avant de le faire ''
Daij-Djan
Pouvez-vous voir ici et les commentaires qui suivent? Je pense que je dis exactement ce que vous dites, c'est-à-dire que nous disons tous les deux que le compilateur veut être sûr plutôt que désolé, ma seule question est, alors comment objective-c a-t-il résolu ce problème? Ou non? Si ce n'est pas le cas, pourquoi faut-il encore que vous écriviez super.initdans la première ligne?
Honey
7

Edward,

Vous pouvez modifier le code dans votre exemple comme ceci:

var playerShip:PlayerShip!
var deltaPoint = CGPointZero

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

Ceci utilise une option implicitement déballée.

Dans la documentation, nous pouvons lire:

"Comme pour les options, si vous ne fournissez pas de valeur initiale lorsque vous déclarez une variable ou une propriété facultative implicitement non encapsulée, sa valeur par défaut est automatiquement nulle."

Pavel Gubarev
la source
Je pense que c'est l'option la plus propre. Dans ma toute première tentative Swift, j'avais une variable membre de type AVCaptureDevice, qui ne peut pas être instanciée directement, donc le code init () était requis. Cependant, ViewController nécessite plusieurs initialiseurs, et vous ne pouvez pas appeler une méthode d'initialisation commune à partir d'init (), donc cette réponse semble être la seule option qui évite de copier / coller du code en double dans chaque initialiseur.
sunetos
6

Swift ne vous permettra pas d'initialiser une super classe sans initialiser les propriétés, à l'inverse de l'Obj C. Vous devez donc initialiser toutes les propriétés avant d'appeler "super.init".

Veuillez vous rendre sur http://blog.scottlogic.com/2014/11/20/swift-initialisation.html . Il donne une belle explication à votre problème.

jishnu bala
la source
6

Ajoutez zéro à la fin de la déclaration.


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

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

Cela a fonctionné pour mon cas, mais peut ne pas fonctionner pour le vôtre

Michael
la source
Bon, tout en utilisant avec UIView avec UIViewController avec protocole
abdul sathar
1

Vous commencez juste dans le mauvais ordre.

     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
la source
0

Je vais probablement recevoir des downvotes, mais pour être honnête, la vie est plus facile de cette façon:

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
la source
-4

C'est une conception incroyablement stupide.

Considérez quelque chose comme ceci:

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

Ceci n'est pas valide, comme indiqué ci-dessus. Mais il en va de même:

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

Parce que «soi» n'a pas été initialisé.

J'espère sincèrement que ce bug sera bientôt corrigé.

(Oui, je sais que je pourrais créer un objet vide, puis définir la taille, mais c'est juste stupide).

Edward Kenworthy
la source
2
Ce n'est pas un bug, sa nouvelle programmation fondamentale des POO.
Hitendra Solanki