J'ai donc mis à jour Xcode 6 beta 5 aujourd'hui et j'ai remarqué que j'avais reçu des erreurs dans presque toutes mes sous-classes des classes d'Apple.
L'erreur indique:
La classe 'x' n'implémente pas les membres requis de sa superclasse
Voici un exemple que j'ai choisi parce que cette classe est actuellement très légère et qu'il sera donc facile de la 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 reçois-je cette erreur et comment puis-je la réparer? Qu'est-ce que je ne mets pas en œuvre? J'appelle un initialiseur désigné.
D'un employé Apple sur les forums de développeur:
"Un moyen de déclarer au compilateur et au programme construit que vous ne voulez vraiment pas être compatible avec 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, cette option est envisageable. J'ai adopté cette approche avec beaucoup de code SpriteKit, car je sais que je ne le chargerai pas à partir d'un story-board.
Une autre option que vous pouvez choisir et qui fonctionne plutôt bien consiste à implémenter la méthode comme une commod init, 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 générer une erreur fatale.
La troisième option est bien sûr d’implémenter la méthode en appelant super et d’initialiser toutes vos propriétés non facultatives. Vous devriez 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)
}
Il y a deux éléments absolument essentiels cruciaux d'informations spécifiques à Swift qui ne figurent pas dans les réponses existantes, ce qui, à mon avis, aidera à clarifier tout cela.
required
.init
.Le tl; dr est ceci:
Si vous implémentez des initialiseurs, vous n'hériterez plus d'aucun des initialiseurs désignés par la superclasse.
Les seuls initialiseurs, le cas échéant, dont vous allez hériter, sont des initialiseurs pratiques de super classe qui pointent vers un initialiseur désigné que vous avez annulé.
Alors ... prêt pour la version longue?
init
.Je sais que c’était le deuxième de mes deux arguments, mais nous ne comprenons pas le premier, ni pourquoi le mot clé required
existe jusqu’à ce que nous comprenions ce point. Une fois que nous avons compris ce point, l’autre devient assez évident.
Toutes les informations que je couvre dans cette section de cette réponse sont issues de la documentation d'Apple trouvée ici .
À partir de la documentation Apple:
Contrairement aux sous-classes dans Objective-C, les sous-classes Swift n'héritent pas de leurs initialiseurs de super-classe par défaut. L'approche de Swift empêche une situation dans laquelle l'initialiseur simple d'une super-classe 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.
L'accent est à moi.
Ainsi, directement à partir de la documentation Apple, vous remarquerez que les sous-classes Swift n'hériteront pas toujours (et généralement pas) des méthodes init
de leur super-classe.
Alors, quand héritent-ils de leur super-classe?
Deux règles définissent quand une sous-classe hérite des méthodes init
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 de super-classe.
Règle 2
Si votre sous-classe fournit une implémentation de tous ses initialiseurs désignés par superclasse, soit en les héritant conformément à la règle 1, soit en fournissant une implémentation personnalisée faisant partie de sa définition, elle hérite automatiquement de tous les initialiseurs de superclasse.
La règle 2 ne concerne pas particulièrement cette conversation car il est peu probable que la fonction init(coder: NSCoder)
de SKSpriteNode
soit une méthode pratique.
Ainsi, votre classe InfoBar
a hérité de l’initialiseur required
jusqu’au moment où vous avez ajouté init(team: Team, size: CGSize)
.
Si vous n'aviez pas fourni cette méthode init
et si vous avez rendu facultative les propriétés ajoutées de votre InfoBar
ou si vous leur avez fourni des valeurs par défaut, vous auriez toujours hérité du init(coder: NSCoder)
de SKSpriteNode
. Cependant, lorsque nous avons ajouté notre propre initialiseur personnalisé, nous avons cessé d'hériter des initialiseurs désignés de notre super-classe (et initialiseurs de commodité , ce qui ne désignait pas 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")
Qui présente l'erreur suivante:
Argument manquant pour le paramètre 'bar' dans l'appel.
Si c'était Objective-C, il n'aurait aucun problème à hériter. Si nous initialisions un Bar
avec initWithFoo:
dans Objective-C, la propriété self.bar
serait simplement nil
. Ce n’est probablement pas génial, mais c’est un état parfaitement valide pour que l’objet soit inséré. C’est pas un état parfaitement valide pour que l'objet Swift soit présent. self.bar
n'est pas facultatif et ne peut pas être nil
.
Encore une fois, la seule façon dont nous héritons des initialiseurs est de ne pas fournir les nôtres. Donc, si nous essayons d'hériter en supprimant la init(foo: String, bar: String)
de Bar
, en tant que telle:
class Bar: Foo {
var bar: String
}
Nous sommes maintenant de retour pour hériter (en quelque sorte), mais cela ne compilera pas ... et le message d'erreur explique exactement pourquoi nous n'héritons pas des méthodes init
de la superclasse:
Problème: La classe 'Bar' n'a pas d'initialiseurs
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 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.
init(coder: NSCoder)
? Pourquoi est-ce required
?Les méthodes init
de Swift peuvent fonctionner avec un ensemble spécial de règles d'héritage, mais la conformité de protocole est toujours héritée dans la chaîne. Si une classe parente est conforme à un protocole, ses sous-classes doivent être conformes à ce protocole.
En règle générale, cela ne pose pas de problème, car la plupart des protocoles requièrent uniquement des méthodes qui ne sont pas gérées par des règles d’héritage particulières dans Swift. Par conséquent, si vous héritez d’une classe conforme à un protocole, vous héritez également de tous les éléments. méthodes ou propriétés permettant à la classe de satisfaire à la conformité de protocole.
Cependant, souvenez-vous que les méthodes init
de Swift sont soumises à un ensemble spécial de règles et ne sont pas toujours héritées. De ce fait, une classe conforme à un protocole nécessitant des méthodes init
spéciales (telles que NSCoding
) nécessite que la classe marque ces méthodes init
comme étant required
.
Considérons 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'initialisation requise 'init (foo :)' ne peut être satisfaite que par un initialiseur 'requis' dans la classe non finale 'ConformingClass'
Fix-It: Insert requis
Il veut que je rende l'initialiseur init(foo: Int)
requis. Je pourrais aussi le rendre heureux en rendant la classe final
(ce qui signifie que la classe ne peut pas être héritée de).
Alors, qu'est-ce qui se passe si je sous-classe? A partir de là, si je sous-classe, ça va. Si j'ajoute cependant des initialiseurs, je n'hérite soudainement plus de init(foo:)
. C'est problématique parce que maintenant je ne me conforme plus à InitProtocol
. Je ne peux pas sous-classer dans une classe conforme à un protocole puis décider soudainement de ne plus vouloir me conformer à ce protocole. J'ai hérité de la conformité au protocole, mais à cause de la façon dont Swift fonctionne avec l'héritage de la méthode init
, je n'ai pas hérité d'une partie de ce qui est nécessaire pour se conformer à ce protocole et je dois donc l'implémenter.
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 protocole hérité NSCoding
et que, pour le résoudre, vous devez 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 de ne pas implémenter ou hériter d’une méthode requise. Outre la conformité au protocole, il existe au moins une autre raison de créer des méthodes init
required
: il s'agit des méthodes d'usine.
Si je veux écrire une méthode de fabrique appropriée, je dois spécifier que le type de retour est Self
(l'équivalent de Swift de instanceType
de Objective-C). Mais pour ce faire, je dois utiliser une méthode d'initialisation required
.
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 du type de classe 'Self' avec une valeur de métatype doit utiliser un initialiseur 'obligatoire'
C'est fondamentalement le même problème. Si nous sous-classe Box
, nos sous-classes hériteront de la méthode de classe factory
. Nous pourrions donc appeler SubclassedBox.factory()
. Cependant, sans le mot clé required
dans la méthode init(size:)
, il n'est pas garanti que les sous-classes de Box
héritent de la self.init(size:)
que factory
appelle.
Nous devons donc créer cette méthode required
si nous voulons une méthode fabrique comme celle-ci. Cela signifie que si notre classe implémente une méthode comme celle-ci, nous aurons une méthode d'initialisation required
et nous rencontrerons exactement les mêmes problèmes que ceux que vous avez exécutés. ici avec le protocole NSCoding
.
En fin de compte, tout se résume à la compréhension de base selon laquelle les initialiseurs de Swift respectent un ensemble légèrement différent de règles d'héritage, ce qui signifie que vous n'êtes pas obligé d'hériter des initialiseurs de votre super-classe. Cela est dû au fait que les initialiseurs de super-classe ne peuvent pas connaître vos nouvelles propriétés stockées et ils ne peuvent pas instancier votre objet dans un état valide. Mais, pour diverses raisons, une super-classe peut marquer un initialiseur avec required
. Lorsque cela se produit, nous pouvons soit utiliser l'un des scénarios très spécifiques par lesquels nous héritons de la méthode required
, soit nous devons l'implémenter nous-mêmes.
Cependant, l’essentiel ici est que si nous obtenons l’erreur que vous voyez ici, cela signifie que votre classe n’implémente pas réellement la méthode.
Comme dernier exemple pour illustrer le fait que les sous-classes Swift n'héritent pas toujours des méthodes init
de leur parent (ce qui, à mon avis, est absolument essentiel à la compréhension complète de 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.
Le message d'erreur qu'il donne est un peu trompeur:
Argument supplémentaire 'b' dans l'appel
Mais le fait est que Bar
n'hérite d'aucune des méthodes Foo
de init
car elle ne satisfait à aucun des deux cas particuliers d'héritage des méthodes init
à partir de sa classe parente.
S'il s'agissait d'Objective-C, nous hériterions de init
sans 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û en être satisfait). Dans Swift, cela ne suffira tout simplement pas. Vous ne pouvez pas avoir un état non valide et les initialiseurs de superclasse hérités ne peuvent conduire qu'à des états d'objet non valides.
Pourquoi ce problème est-il apparu? Eh bien, le fait est que cela a toujours été important (c'est-à-dire en Objective-C depuis le jour où j'ai commencé à programmer Cocoa dans Mac OS X 10.0) pour traiter les initialiseurs auxquels votre classe n'est pas préparée. gérer. Les docs 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 d'entre nous! Et le compilateur ne les a pas imposées; 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 essentiel que nous recevions une collection d'éléments multimédias: l'instance ne peut tout simplement pas exister sans une. Mais je n’ai écrit aucun "bouchon" pour empêcher quelqu'un de m'initialiser avec des os nus init
à la place. I devrait en ai écrit un (en fait, j'aurais dû écrire une implémentation de initWithNibName:bundle:
, l'initialiseur désigné hérité); mais j'étais trop paresseux pour le déranger, parce que je "savais" que je n'initialiserais jamais de façon incorrecte ma propre classe de cette façon. Cela a laissé un trou béant. En Objective-C, quelqu'un peut appelle les éléments de base init
, laissant mes ivars non initialisés et nous remontons le ruisseau sans Paddle.
Rapide, merveilleusement, me sauve de moi dans la plupart des cas. Dès que j'ai traduit cette application en Swift, tout le problème a disparu. Swift crée effectivement 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 compris que le miracle ne fonctionnait 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 empêcher cela - et à quel moment le nib se charge, init(coder:)
sera appelé. Donc, le compilateur vous fait écrire le bouchon explicitement. Et tout à fait aussi.
ajouter
required init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}