La classe n'implémente pas les membres requis de sa superclasse

155

J'ai donc mis à jour vers Xcode 6 beta 5 aujourd'hui et j'ai remarqué que j'avais reçu des erreurs dans presque toutes mes sous-classes de classes Apple.

L'erreur déclare:

La classe 'x' n'implémente pas les membres requis de sa superclasse

Voici un exemple que j'ai choisi car cette classe est actuellement assez légère, donc elle sera facile à publier.

class InfoBar: SKSpriteNode  { //Error message here

    let team: Team
    let healthBar: SKSpriteNode

    init(team: Team, size: CGSize) {
        self.team = team
        if self.team == Team.TeamGood {
            healthBar = SKSpriteNode(color: UIColor.greenColor(), size:size)
        }
        else {
            healthBar = SKSpriteNode(color: UIColor.redColor(), size:size)
        }
        super.init(texture:nil, color: UIColor.darkGrayColor(), size: size)

        self.addChild(healthBar)

    }

}

Ma question est donc la suivante: pourquoi est-ce que je reçois cette erreur et comment puis-je la corriger? Qu'est-ce que je ne mets pas en œuvre? J'appelle un initialiseur désigné.

Octet épique
la source

Réponses:

127

D'un employé Apple sur les forums des développeurs:

"Une façon de déclarer au compilateur et au programme construit que vous ne voulez vraiment pas être compatible NSCoding est de faire quelque chose comme ceci:"

required init(coder: NSCoder) {
  fatalError("NSCoding not supported")
}

Si vous savez que vous ne voulez pas être compatible NSCoding, c'est une option. J'ai adopté cette approche avec une grande partie de mon code SpriteKit, car je sais que je ne le chargerai pas à partir d'un storyboard.


Une autre option que vous pouvez prendre et qui fonctionne plutôt bien est d'implémenter la méthode comme un init pratique, comme ceci:

convenience required init(coder: NSCoder) {
    self.init(stringParam: "", intParam: 5)
}

Notez l'appel à un initialiseur dans self. Cela vous permet de n'utiliser que des valeurs factices pour les paramètres, par opposition à toutes les propriétés non facultatives, tout en évitant de lancer une erreur fatale.


La troisième option est bien sûr d'implémenter la méthode lors de l'appel de super et d'initialiser toutes vos propriétés non optionnelles. Vous devez adopter cette approche si l'objet est une vue en cours de chargement à partir d'un storyboard:

required init(coder aDecoder: NSCoder!) {
    foo = "some string"
    bar = 9001

    super.init(coder: aDecoder)
}
Ben Kane
la source
3
La deuxième option est cependant inutile dans la plupart des cas réels. Prenez, par exemple, mon initialiseur requis init(collection:MPMediaItemCollection). Vous devez fournir une véritable collection d'articles multimédias; c'est le but de cette classe. Cette classe ne peut tout simplement pas être instanciée sans un. Il va analyser la collection et initialiser une douzaine de variables d'instance. C'est tout l'intérêt de ceci étant le seul et unique initialiseur désigné! Ainsi, init(coder:)n'a aucun MPMediaItemCollection significatif (ou même dénué de sens) à fournir ici; seule l' fatalErrorapproche est la bonne.
mat
@matt Correct, l'une ou l'autre option fonctionnera mieux dans différentes situations.
Ben Kane
C'est vrai, et j'ai découvert et examiné la deuxième option indépendamment, et parfois cela aura du sens. Par exemple, j'aurais pu déclarer mon di init(collection:MPMediaItemCollection!). Cela permettrait init(coder:)de passer nul. Mais alors j'ai réalisé: non, maintenant vous trompez simplement le compilateur. Passer nul n'est pas acceptable, alors lancez le fatalErroret continuez. :)
mat
1
Je sais que cette question et ses réponses sont un peu anciennes maintenant, mais j'ai publié une nouvelle réponse qui aborde certains points qui, à mon avis, sont cruciaux pour comprendre réellement cette erreur qui n'a été abordée par aucune des réponses existantes.
nhgrif
Bonne réponse. Je suis d'accord avec vous qu'il est essentiel de comprendre que Swift n'hérite pas toujours de super initialiseurs pour comprendre ce modèle.
Ben Kane
71

Il y a deux éléments absolument cruciaux d'informations spécifiques à Swift qui manquent dans les réponses existantes et qui, je pense, aident à éclaircir complètement cela.

  1. Si un protocole spécifie un initialiseur comme méthode requise, cet initialiseur doit être marqué à l'aide du requiredmot - clé de Swift .
  2. Swift a un ensemble spécial de règles d'héritage concernant les initméthodes.

Le tl; dr est le suivant:

Si vous implémentez des initialiseurs, vous n'héritez plus d'aucun des initialiseurs désignés de la superclasse.

Les seuls initialiseurs, le cas échéant, dont vous hériterez, sont des initialiseurs pratiques de super classe qui pointent vers un initialiseur désigné que vous avez remplacé.

Alors ... prêt pour la version longue?


Swift a un ensemble spécial de règles d'héritage concernant les initméthodes.

Je sais que c'était le deuxième de deux points que j'ai soulevés, mais nous ne pouvons pas comprendre le premier point, ni pourquoi le requiredmot-clé existe même jusqu'à ce que nous comprenions ce point. Une fois que nous comprenons ce point, l'autre devient assez évident.

Toutes les informations que je couvre dans cette section de cette réponse proviennent de la documentation d'Apple disponible ici .

À partir de la documentation Apple:

Contrairement aux sous-classes d'Objective-C, les sous-classes Swift n'héritent pas par défaut de leurs initialiseurs de superclasse. L'approche de Swift empêche une situation dans laquelle un simple initialiseur d'une superclasse est hérité par une sous-classe plus spécialisée et est utilisé pour créer une nouvelle instance de la sous-classe qui n'est pas complètement ou correctement initialisée.

Soulignez le mien.

Donc, directement à partir de la documentation Apple, nous voyons que les sous-classes Swift n'hériteront pas toujours (et généralement pas) des initméthodes de leur superclasse .

Alors, quand héritent-ils de leur superclasse?

Il existe deux règles qui définissent le moment où une sous-classe hérite des initméthodes de son parent. À partir de la documentation Apple:

Règle 1

Si votre sous-classe ne définit aucun initialiseur désigné, elle hérite automatiquement de tous ses initialiseurs désignés par superclasse.

Règle 2

Si votre sous-classe fournit une implémentation de tous ses initialiseurs désignés de superclasse - soit en les héritant selon la règle 1, soit en fournissant une implémentation personnalisée dans le cadre de sa définition - alors elle hérite automatiquement de tous les initialiseurs de commodité de la superclasse.

Règle 2 ne sont pas particulièrement à cette conversation , car SKSpriteNodel » init(coder: NSCoder)est peu susceptible d'être une méthode pratique.

Ainsi, votre InfoBarclasse héritait de l' requiredinitialiseur jusqu'au point que vous avez ajouté init(team: Team, size: CGSize).

Si vous n'aviez pas fourni cette initméthode et à la place InfoBar, vous aviez rendu les propriétés ajoutées facultatives ou leur fournir des valeurs par défaut, alors vous auriez toujours hérité SKSpriteNodede celles init(coder: NSCoder). Cependant, lorsque nous avons ajouté notre propre initialiseur personnalisé, nous avons cessé d'hériter des initialiseurs désignés de notre superclasse (et des initialiseurs pratiques qui ne pointaient pas vers les initialiseurs que nous avons implémentés).

Donc, à titre d'exemple simpliste, je présente ceci:

class Foo {
    var foo: String
    init(foo: String) {
        self.foo = foo
    }
}

class Bar: Foo {
    var bar: String
    init(foo: String, bar: String) {
        self.bar = bar
        super.init(foo: foo)
    }
}


let x = Bar(foo: "Foo")

Ce qui présente l'erreur suivante:

Argument manquant pour le paramètre «bar» dans l'appel.

entrez la description de l'image ici

Si c'était Objective-C, il n'aurait aucun problème à hériter. Si nous initialisions a Baravec initWithFoo:en Objective-C, la self.barpropriété serait simplement nil. Ce n'est probablement pas génial, mais c'est un état parfaitement valide pour l'objet. Ce n'est pas un état parfaitement valide pour l'objet Swift. Ce self.barn'est pas facultatif et ne peut pas l'être nil.

Encore une fois, la seule façon d'hériter des initialiseurs est de ne pas fournir les nôtres. Donc, si nous essayons d'hériter en supprimant Bardes init(foo: String, bar: String), en tant que tels:

class Bar: Foo {
    var bar: String
}

Nous revenons maintenant à l'héritage (en quelque sorte), mais cela ne compilera pas ... et le message d'erreur explique exactement pourquoi nous n'héritons pas des initméthodes de superclasse :

Problème: la classe 'Bar' n'a pas d'initialiseur

Fix-It: La propriété stockée 'bar' sans initialiseurs empêche les initialiseurs synthétisés

Si nous avons ajouté des propriétés stockées dans notre sous-classe, il n'y a pas de moyen Swift possible de créer une instance valide de notre sous-classe avec les initialiseurs de superclasse qui ne pourraient pas connaître les propriétés stockées de notre sous-classe.


Ok, eh bien, pourquoi dois-je mettre init(coder: NSCoder)en œuvre du tout? Pourquoi ça required?

Les initméthodes de Swift peuvent jouer avec un ensemble spécial de règles d'héritage, mais la conformité du protocole est toujours héritée le long de la chaîne. Si une classe parente est conforme à un protocole, ses sous-classes doivent se conformer à ce protocole.

Normalement, ce n'est pas un problème, car la plupart des protocoles ne nécessitent que des méthodes qui ne fonctionnent pas avec des règles d'héritage spéciales dans Swift, donc si vous héritez d'une classe conforme à un protocole, vous héritez également de tous les méthodes ou propriétés qui permettent à la classe de satisfaire la conformité du protocole.

Cependant, rappelez-vous que les initméthodes de Swift fonctionnent selon un ensemble spécial de règles et ne sont pas toujours héritées. Pour cette raison, une classe conforme à un protocole qui requiert des initméthodes spéciales (telles que NSCoding) requiert que la classe marque ces initméthodes comme required.

Prenons cet exemple:

protocol InitProtocol {
    init(foo: Int)
}

class ConformingClass: InitProtocol {
    var foo: Int
    init(foo: Int) {
        self.foo = foo
    }
}

Cela ne compile pas. Il génère l'avertissement suivant:

Problème: l' exigence d'initialisation 'init (foo :)' ne peut être satisfaite que par un initialiseur 'requis' dans la classe non finale 'ConformingClass'

Fix-It: Insertion requise

Il veut que je fasse l' init(foo: Int)initialiseur requis. Je pourrais aussi le rendre heureux en créant la classe final(ce qui signifie que la classe ne peut pas être héritée).

Alors, que se passe-t-il si je sous-classe? À partir de là, si je sous-classe, je vais bien. Cependant, si j'ajoute des initialiseurs, je n'hérite plus init(foo:). Ceci est problématique car maintenant je ne suis plus conforme au InitProtocol. Je ne peux pas faire de sous-classe à partir d'une classe conforme à un protocole, puis décider soudainement que je ne veux plus me conformer à ce protocole. J'ai hérité de la conformité du protocole, mais à cause de la façon dont Swift fonctionne avec l' inithéritage de méthode, je n'ai pas hérité d'une partie de ce qui est nécessaire pour se conformer à ce protocole et je dois l'implémenter.


D'accord, tout cela a du sens. Mais pourquoi ne puis-je pas recevoir un message d'erreur plus utile?

On peut soutenir que le message d'erreur pourrait être plus clair ou meilleur s'il spécifiait que votre classe n'était plus conforme au NSCodingprotocole hérité et que pour le corriger, vous devez l'implémenter init(coder: NSCoder). Sûr.

Mais Xcode ne peut tout simplement pas générer ce message car ce ne sera pas toujours le problème réel de ne pas implémenter ou d'hériter d'une méthode requise. Il y a au moins une autre raison de créer des initméthodes en requiredplus de la conformité du protocole, et ce sont les méthodes d'usine.

Si je veux écrire une méthode d'usine appropriée, je dois spécifier le type de retour à être Self(l'équivalent de Swift d'Objective-C instanceType). Mais pour ce faire, je dois en fait utiliser une requiredméthode d'initialisation.

class Box {
    var size: CGSize
    init(size: CGSize) {
        self.size = size
    }

    class func factory() -> Self {
        return self.init(size: CGSizeZero)
    }
}

Cela génère l'erreur:

La construction d'un objet de type de classe 'Self' avec une valeur de métatype doit utiliser un initialiseur 'obligatoire'

entrez la description de l'image ici

C'est fondamentalement le même problème. Si nous sous Box- classes , nos sous-classes hériteront de la méthode de classe factory. Nous pourrions donc appeler SubclassedBox.factory(). Cependant, sans le requiredmot clé sur la init(size:)méthode, Boxles sous-classes de s ne sont pas garanties d'hériter de celle self.init(size:)qui factoryappelle.

Nous devons donc créer cette méthode requiredsi nous voulons une méthode d'usine comme celle-ci, et cela signifie que si notre classe implémente une méthode comme celle-ci, nous aurons une requiredméthode d'initialisation et nous rencontrerons exactement les mêmes problèmes que vous avez rencontrés ici avec le NSCodingprotocole.


En fin de compte, tout se résume à la compréhension de base que les initialiseurs de Swift jouent par un ensemble légèrement différent de règles d'héritage, ce qui signifie que vous n'êtes pas assuré d'hériter des initialiseurs de votre superclasse. Cela se produit car les initialiseurs de superclasse ne peuvent pas connaître vos nouvelles propriétés stockées et ne peuvent pas instancier votre objet dans un état valide. Mais, pour diverses raisons, une superclasse peut marquer un initialiseur comme required. Quand c'est le cas, nous pouvons soit utiliser l'un des scénarios très spécifiques par lesquels nous héritons réellement de la requiredméthode, soit nous devons l'implémenter nous-mêmes.

Le point principal ici est que si nous obtenons l'erreur que vous voyez ici, cela signifie que votre classe n'implémente pas du tout la méthode.

Comme peut-être un dernier exemple pour explorer le fait que les sous-classes Swift n'héritent pas toujours des initméthodes de leurs parents (ce qui, je pense, est absolument essentiel pour comprendre pleinement ce problème), considérons cet exemple:

class Foo {
    init(a: Int, b: Int, c: Int) {
        // do nothing
    }
}

class Bar: Foo {
    init(string: String) {
        super.init(a: 0, b: 1, c: 2)
        // do more nothing
    }
}

let f = Foo(a: 0, b: 1, c: 2)
let b = Bar(a: 0, b: 1, c: 2)

Cela échoue à compiler.

entrez la description de l'image ici

Le message d'erreur qu'il donne est un peu trompeur:

Argument supplémentaire 'b' dans l'appel

Mais le point est, Barne hérite pas de Foode » initméthodes parce qu'elle n'a pas satisfait à l'une des deux cas particuliers pour hériter des initméthodes de la classe parente.

Si c'était Objective-C, nous en hériterions initsans problème, car Objective-C est parfaitement heureux de ne pas initialiser les propriétés des objets (bien qu'en tant que développeur, vous n'auriez pas dû être satisfait de cela). Dans Swift, cela ne fonctionnera tout simplement pas. Vous ne pouvez pas avoir un état non valide et l'héritage d'initialiseurs de superclasse ne peut conduire qu'à des états d'objet non valides.

nhgrif
la source
Pouvez-vous expliquer ce que signifie cette phrase ou donner un exemple? "(et des initialiseurs de commodité qui ne pointaient pas vers les initialiseurs que nous avons implémentés)"
Abbey Jackson
Réponse géniale! Je souhaite que plus de messages SO parlent de pourquoi , comme celui-ci, au lieu de seulement comment .
Alexander Vasenin
56

Pourquoi ce problème s'est-il posé? Eh bien, le fait est qu'il a toujours été important (c'est-à-dire en Objective-C, depuis le jour où j'ai commencé à programmer Cocoa sous Mac OS X 10.0) de gérer les initialiseurs que votre classe n'est pas prête à gérer. Les documents ont toujours été très clairs sur vos responsabilités à cet égard. Mais combien d'entre nous ont pris la peine de les remplir, complètement et à la lettre? Probablement aucun de nous! Et le compilateur ne les a pas appliqués; tout était purement conventionnel.

Par exemple, dans ma sous-classe de contrôleur de vue Objective-C avec cet initialiseur désigné:

- (instancetype) initWithCollection: (MPMediaItemCollection*) coll;

... il est crucial de nous transmettre une véritable collection d'éléments multimédias: l'instance ne peut tout simplement pas exister sans une. Mais je n'ai pas écrit de «stopper» pour empêcher quelqu'un de m'initialiser à la initplace. Je devrais avoir écrit un ( en fait, à proprement parler, je l' ai écrit une mise en oeuvre initWithNibName:bundle:, l'initialiseur désigné hérité); mais j'étais trop paresseux pour me déranger, parce que je "savais" que je n'initialiserais jamais incorrectement ma propre classe de cette façon. Cela a laissé un trou béant. En Objective-C, quelqu'un peut appeler bare-bones init, laissant mes ivars non initialisés, et nous remontons le ruisseau sans pagaie.

Swift, merveilleusement, me sauve de moi-même dans la plupart des cas. Dès que j'ai traduit cette application en Swift, tout le problème a disparu. Swift crée efficacement un bouchon pour moi! Si init(collection:MPMediaItemCollection)est le seul initialiseur désigné déclaré dans ma classe, je ne peux pas être initialisé en appelant bare-bones init(). C'est un miracle!

Ce qui s'est passé dans la graine 5, c'est simplement que le compilateur a réalisé que le miracle ne fonctionne pas dans le cas de init(coder:), car en théorie, une instance de cette classe pourrait provenir d'un nib, et le compilateur ne peut pas l'empêcher - et quand le nib charges, init(coder:)sera appelée. Ainsi, le compilateur vous oblige à écrire explicitement le stopper. Et tout à fait raison aussi.

mat
la source
Merci pour cette réponse détaillée. Cela apporte vraiment la lumière sur la question.
Julian Osorio
Un vote positif à pasta12 pour m'avoir dit comment faire taire le compilateur, mais un vote positif à vous aussi pour m'avoir compris ce dont il se plaignait au départ.
Garrett Albright
2
Trou béant ou pas, je n'allais jamais appeler cela init, il est donc totalement officieux de me forcer à l'inclure. Le code gonflé est une surcharge dont aucun de nous n'a besoin. Cela vous oblige également à initialiser vos propriétés dans les deux inits. Inutile!
Dan Greenfield
5
@DanGreenfield Non, cela ne vous oblige pas à initialiser quoi que ce soit, car si vous n'allez jamais l'appeler, il vous suffit de mettre le fatalErrorbouchon décrit dans stackoverflow.com/a/25128815/341994 . Faites-en simplement un extrait de code utilisateur et à partir de maintenant, vous pouvez simplement le placer là où vous en avez besoin. Prend une demi-seconde.
mat
1
@nhgrif Eh bien, pour être honnête, la question ne demandait pas l'histoire complète. Il s'agissait simplement de savoir comment sortir de cet embouteillage et passer à autre chose. L'histoire complète est donnée dans mon livre: apeth.com/swiftBook/ch04.html#_class_initializers
mat
33

ajouter

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
la source
3
Cela fonctionne, mais je ne pense pas que ce soit un bug. les initialiseurs ne sont pas hérités dans swift (lorsque votre propre initialiseur est déclaré) et ceci est marqué avec le mot-clé requis. Le seul problème est que maintenant je dois initialiser TOUTES mes propriétés dans cette méthode pour chacune de mes classes, ce qui sera beaucoup de code gaspillé car je ne l'utilise pas du tout. Ou je devrai déclarer toutes mes propriétés comme des types optionnels implicitement non emballés pour contourner l'initialisation, ce que je ne veux pas non plus faire.
Epic Byte
1
Ouaip! J'ai réalisé aussitôt après avoir dit que c'était peut-être un bug, que cela avait un sens logique. Je suis d'accord que ce sera beaucoup de code gaspillé, car comme vous, je n'utiliserais jamais cette méthode init. Pas encore sûr d'une solution élégante
Gagan Singh
2
J'ai eu le même problème. Cela a du sens avec "requis init", mais Swift n'est pas le langage "facile" que j'espérais. Tous ces «optionnels» rendent le langage plus complexe que nécessaire. Et pas de support pour DSL et AOP. Je suis de plus en plus déçu.
user810395
2
Oui, je suis entièrement d'accord. Beaucoup de mes propriétés sont maintenant déclarées comme optionnelles parce que je suis obligé de le faire, alors qu'en réalité elles ne devraient pas être autorisées à être nulles. Certains sont optionnels parce qu'ils devraient légitimement être optionnels (ce qui signifie que nul EST une valeur valide). Et puis dans les classes où je ne sous-classe pas, je n'ai pas besoin d'utiliser des options, donc les choses deviennent très complexes, et je n'arrive pas à trouver un bon style de codage. Espérons qu'Apple trouve quelque chose.
Epic Byte
5
Je pense qu'ils signifient que vous pouvez satisfaire l'initialiseur requis en ne déclarant aucun initialiseur de votre choix, ce qui entraînerait l'héritage de tous les initialiseurs.
Epic Byte