ios - thumbnail - La clase no implementa los miembros requeridos de su superclase
ion thumbnail size (4)
Así que actualicé a Xcode 6 beta 5 hoy y noté que recibí errores en casi todas mis subclases de las clases de Apple.
El error dice:
La clase ''x'' no implementa los miembros requeridos de su superclase
Aquí hay un ejemplo que escogí porque esta clase actualmente es bastante liviana por lo que será fácil de publicar.
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)
}
}
Entonces mi pregunta es, ¿por qué recibo este error y cómo puedo solucionarlo? ¿Qué es lo que no estoy implementando? Estoy llamando a un inicializador designado.
¿Por qué ha surgido este problema? Bueno, la verdad es que siempre ha sido importante (es decir, en Objective-C, desde el día en que comencé a programar Cocoa en Mac OS X 10.0) para tratar con los inicializadores que tu clase no está preparada para manejar. Los documentos siempre han sido muy claros acerca de sus responsabilidades en este sentido. Pero, ¿cuántos de nosotros nos molestamos en cumplirlos, por completo y al pie de la letra? ¡Probablemente ninguno de nosotros! Y el compilador no los hizo cumplir; era todo puramente convencional.
Por ejemplo, en mi subclase de controlador de vista Objective-C con este inicializador designado:
- (instancetype) initWithCollection: (MPMediaItemCollection*) coll;
... es crucial que se nos pase una colección real de elementos multimedia: la instancia simplemente no puede existir sin una. Pero no he escrito "stopper" para evitar que alguien me inicie con init
en init
. Debería haber escrito uno (en realidad, propiamente hablando, debería haber escrito una implementación de initWithNibName:bundle:
el inicializador designado heredado); pero era demasiado perezoso para molestarme, porque "sabía" que nunca inicializaría incorrectamente mi propia clase de esa manera. Esto dejó un agujero enorme. En Objective-C, alguien puede llamar a init
bare-bones, dejando mis ivars sin inicializar, y estamos en el arroyo sin una paleta.
Swift, maravillosamente, me salva de mí en la mayoría de los casos. Tan pronto como traduje esta aplicación a Swift, todo el problema desapareció. ¡Swift efectivamente crea un stopper para mí! Si init(collection:MPMediaItemCollection)
es el único inicializador designado declarado en mi clase, no puedo inicializarlo llamando a bare-bones init()
. ¡Es un milagro!
Lo que sucedió en la semilla 5 es simplemente que el compilador se dio cuenta de que el milagro no funciona en el caso de init(coder:)
, porque en teoría una instancia de esta clase podría provenir de un plumín, y el compilador no puede prevenir eso - y cuando el plumín se carga, se llamará init(coder:)
. Entonces el compilador te hace escribir el stopper explícitamente. Y muy bien también.
De un empleado de Apple en los foros de desarrolladores:
"Una forma de declarar al compilador y al programa construido que realmente no quieres ser compatible con NSCoding es hacer algo como esto:"
required init(coder: NSCoder) {
fatalError("NSCoding not supported")
}
Si sabe que no quiere cumplir con NSCoding, esta es una opción. He adoptado este enfoque con una gran cantidad de mi código de SpriteKit, ya que sé que no lo cargaré desde un guión gráfico.
Otra opción que puede tomar y que funciona bastante bien es implementar el método como un inicio de conveniencia, como sigue:
convenience required init(coder: NSCoder) {
self.init(stringParam: "", intParam: 5)
}
Tenga en cuenta la llamada a un inicializador en self
. Esto le permite usar solo valores ficticios para los parámetros, a diferencia de todas las propiedades no opcionales, mientras evita lanzar un error fatal.
La tercera opción, por supuesto, es implementar el método al llamar a super e inicializar todas sus propiedades no opcionales. Debe tomar este enfoque si el objeto es una vista que se carga desde un guión gráfico:
required init(coder aDecoder: NSCoder!) {
foo = "some string"
bar = 9001
super.init(coder: aDecoder)
}
Hay dos piezas absolutamente cruciales de información específica de Swift que faltan en las respuestas existentes, que creo que ayudan a aclarar esto por completo.
- Si un protocolo especifica un inicializador como un método requerido, ese inicializador debe marcarse usando la palabra clave
required
de Swift. - Swift tiene un conjunto especial de reglas de herencia con respecto a los métodos
init
.
El tl; dr es esto:
Si implementa cualquier inicializador, ya no heredará ninguno de los inicializadores designados de la superclase.
Los únicos inicializadores, en su caso, que heredará, son los inicializadores de conveniencia de superclase que apuntan a un inicializador designado que usted anuló.
Entonces ... ¿listo para la versión larga?
Swift tiene un conjunto especial de reglas de herencia con respecto a los métodos init
.
Sé que este fue el segundo de dos puntos que hice, pero no podemos entender el primer punto, o por qué la palabra clave required
incluso existe hasta que comprendamos este punto. Una vez que entendemos este punto, el otro se vuelve bastante obvio.
Toda la información que cubro en esta sección de esta respuesta proviene de la documentación de Apple que se encuentra here .
De los documentos de Apple:
A diferencia de las subclases en Objective-C, las subclases Swift no heredan sus inicializadores de superclase de forma predeterminada. El enfoque de Swift previene una situación en la que una inicialización más simple de una superclase es heredada por una subclase más especializada y se utiliza para crear una nueva instancia de la subclase que no se ha inicializado completa o correctamente.
Énfasis mío
Entonces, directamente desde los documentos de Apple, vemos que las subclases de Swift no siempre (y normalmente no) heredan los métodos de init
su superclase.
Entonces, ¿cuándo heredan de su superclase?
Hay dos reglas que definen cuándo una subclase hereda los métodos de init
de su padre. De los documentos de Apple:
Regla 1
Si su subclase no define ningún inicializador designado, hereda automáticamente todos sus inicializadores designados de superclase.
Regla 2
Si su subclase proporciona una implementación de todos sus inicializadores designados de superclase, ya sea heredando según la regla 1 o proporcionando una implementación personalizada como parte de su definición, entonces heredará automáticamente todos los inicializadores de conveniencia de la superclase.
La regla 2 no es particularmente relevante para esta conversación porque el init(coder: NSCoder)
es poco probable que sea un método conveniente.
Entonces, su clase InfoBar
heredaba el inicializador required
hasta el momento en que agregó init(team: Team, size: CGSize)
.
Si no hubiera proporcionado este método de init
y, en su lugar, ha hecho que las propiedades añadidas de su InfoBar
opcionales o les haya proporcionado valores predeterminados, entonces aún habría heredado el init(coder: NSCoder)
. Sin embargo, cuando agregamos nuestro propio inicializador personalizado, dejamos de heredar los inicializadores designados de nuestra superclase (y los inicializadores de conveniencia que no apuntaban a los inicializadores que implementamos).
Entonces, como un ejemplo simplista, presento esto:
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")
Que presenta el siguiente error:
Falta el argumento para el parámetro ''bar'' en la llamada.
Si esto fuera Objective-C, no tendría problemas para heredar. Si inicializamos una Bar
con initWithFoo:
en Objective-C, la propiedad self.bar
sería simplemente nil
. Probablemente no sea genial, pero es un estado perfectamente válido para que el objeto esté dentro. No es un estado perfectamente válido para que el objeto Swift esté. self.bar
no es una opción y no puede ser nil
.
Nuevamente, la única forma en que heredamos los inicializadores es no proporcionándonos el nuestro. Entonces, si tratamos de heredar borrando el init(foo: String, bar: String)
Bar
init(foo: String, bar: String)
, como tal:
class Bar: Foo {
var bar: String
}
Ahora volvemos a heredar (más o menos), pero esto no se compilará ... y el mensaje de error explica exactamente por qué no heredamos los métodos init
superclase:
Problema: la clase ''Bar'' no tiene inicializadores
Fix-It: la propiedad almacenada ''barra'' sin inicializadores evita los inicializadores sintetizados
Si agregamos propiedades almacenadas en nuestra subclase, no hay forma posible de Swift de crear una instancia válida de nuestra subclase con los inicializadores de la superclase que posiblemente no puedan conocer las propiedades almacenadas de nuestra subclase.
De acuerdo, bueno, ¿por qué tengo que implementar init(coder: NSCoder)
en absoluto? ¿Por qué es required
?
Los métodos de init
de Swift pueden jugar con un conjunto especial de reglas de herencia, pero la conformidad del protocolo aún se hereda por la cadena. Si una clase principal cumple con un protocolo, sus subclases deben cumplir con ese protocolo.
Normalmente, esto no es un problema, porque la mayoría de los protocolos solo requieren métodos que no se rigen por reglas de herencia especiales en Swift, por lo que si hereda de una clase que se ajusta a un protocolo, también heredará todas las métodos o propiedades que permiten que la clase satisfaga la conformidad del protocolo.
Sin embargo, recuerde, los métodos de init
de Swift se rigen por un conjunto especial de reglas y no siempre se heredan. Debido a esto, una clase que se ajuste a un protocolo que requiera métodos de init
especiales (como NSCoding
) requiere que la clase marque esos métodos init
según sea required
.
Considera este ejemplo:
protocol InitProtocol {
init(foo: Int)
}
class ConformingClass: InitProtocol {
var foo: Int
init(foo: Int) {
self.foo = foo
}
}
Esto no compila. Genera la siguiente advertencia:
Problema: el requisito del inicializador ''init (foo :)'' solo puede cumplirse mediante un inicializador ''requerido'' en la clase no final ''ConformingClass''
Fix-It: Insert requerido
Quiere que haga el init(foo: Int)
requerido. También podría hacerlo feliz haciendo que la clase sea final
(lo que significa que no se puede heredar la clase).
Entonces, ¿qué pasa si subclase? A partir de este punto, si hago una subclase, estoy bien. Sin embargo, si agrego cualquier inicializador, de repente ya no heredo init(foo:)
. Esto es problemático porque ahora ya no me estoy InitProtocol
al InitProtocol
. No puedo crear una subclase de una clase que se ajuste a un protocolo y luego decidir de repente que ya no quiero conformarme a ese protocolo. Heredé la conformidad del protocolo, pero debido a la forma en que Swift funciona con la herencia del método init
, no he heredado parte de lo que se requiere para cumplir con ese protocolo y debo implementarlo.
De acuerdo, todo esto tiene sentido. ¿Pero por qué no puedo obtener un mensaje de error más útil?
Podría decirse que el mensaje de error podría ser más claro o mejor si especificaba que su clase ya no se ajustaba al protocolo NSCoding
heredado y que para solucionarlo debe implementar init(coder: NSCoder)
. Por supuesto.
Pero Xcode simplemente no puede generar ese mensaje porque eso no siempre será el problema real al no implementar o heredar un método requerido. Hay al menos otro motivo para hacer que se required
métodos init
además de la conformidad del protocolo, y eso son métodos de fábrica.
Si quiero escribir un método de fábrica adecuado, necesito especificar el tipo de devolución para que sea Self
(el equivalente de Swift del instanceType
de Objective-C). Pero para hacer esto, realmente necesito usar un método de inicialización required
.
class Box {
var size: CGSize
init(size: CGSize) {
self.size = size
}
class func factory() -> Self {
return self.init(size: CGSizeZero)
}
}
Esto genera el error:
La construcción de un objeto del tipo de clase ''Self'' con un valor de metatipo debe usar un inicializador ''requerido''
Básicamente es el mismo problema. Si subclasificamos Box
, nuestras subclases heredarán la factory
método de clase. Entonces podríamos llamar a SubclassedBox.factory()
. Sin embargo, sin la palabra clave required
en el método init(size:)
, no se garantiza que las subclases de Box
hereden el self.init(size:)
que está llamando la factory
.
Entonces, debemos hacer que ese método sea required
si queremos un método de fábrica como este, y eso significa que si nuestra clase implementa un método como este, tendremos un método de inicialización required
y nos encontraremos con exactamente los mismos problemas que ha ejecutado aquí con el protocolo NSCoding
.
En última instancia, todo se reduce a la comprensión básica de que los inicializadores de Swift juegan con un conjunto ligeramente diferente de reglas de herencia, lo que significa que no está garantizado que herede los inicializadores de su superclase. Esto sucede porque los inicializadores de superclase no pueden conocer sus nuevas propiedades almacenadas y no pudieron crear una instancia de su objeto en un estado válido. Pero, por diversas razones, una superclase puede marcar un inicializador según sea required
. Cuando lo hace, podemos emplear uno de los escenarios muy específicos por los cuales realmente heredamos el método required
, o debemos implementarlo nosotros mismos.
El punto principal aquí es que si obtenemos el error que ves aquí, significa que tu clase no está implementando el método en absoluto.
Como quizás un último ejemplo para profundizar en el hecho de que las subclases de Swift no siempre heredan los métodos de init
sus padres (que creo que son absolutamente fundamentales para comprender completamente este problema), considere este ejemplo:
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)
Esto no se puede compilar.
El mensaje de error que da es un poco engañoso:
Argumento extra ''b'' en llamada
Pero el punto es que Bar
no hereda ninguno de los métodos init
de Foo
porque no ha satisfecho ninguno de los dos casos especiales para heredar los métodos init
de su clase padre.
Si esto fuera Objective-C, heredaríamos ese init
sin ningún problema, porque Objective-C está perfectamente feliz de no inicializar las propiedades de los objetos (aunque como desarrollador, no debería haber estado contento con esto). En Swift, esto simplemente no funciona. No puede tener un estado no válido y la herencia de inicializadores de superclase solo puede generar estados de objeto no válidos.
añadir
required init(coder aDecoder: NSCoder!) {
super.init(coder: aDecoder)
}