La clase no implementa los miembros requeridos de su superclase

155

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 elegí porque esta clase es bastante ligera, por lo que será fácil publicarla.

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.

Byte épico
fuente

Respuestas:

127

De un empleado de Apple en los foros de desarrolladores:

"Una forma de declarar al compilador y al programa construido que realmente no desea ser compatible con NSCoding es hacer algo como esto:"

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

Si sabe que no quiere ser compatible con NSCoding, esta es una opción. He adoptado este enfoque con gran parte de mi código SpriteKit, ya que sé que no lo cargaré desde un guión gráfico.


Otra opción que puede tomar que funciona bastante bien es implementar el método como un inicio conveniente, de esta manera:

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, en oposición a todas las propiedades no opcionales, mientras evita arrojar un error fatal.


La tercera opción, por supuesto, es implementar el método mientras se llama a super e inicializar todas sus propiedades no opcionales. Debería adoptar este enfoque si el objeto es una vista que se está cargando desde un guión gráfico:

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

    super.init(coder: aDecoder)
}
Ben Kane
fuente
3
Sin embargo, la segunda opción es inútil en la mayoría de los casos de la vida real. Tome, por ejemplo, mi inicializador requerido init(collection:MPMediaItemCollection). Debe proporcionar una colección real de elementos de medios; ese es el punto de esta clase. Esta clase simplemente no puede ser instanciada sin una. Analizará la colección e inicializará una docena de variables de instancia. ¡Ese es el objetivo de que este sea el único inicializador designado! Por lo tanto, init(coder:)no tiene MPMediaItemCollection significativo (o incluso sin sentido) para suministrar aquí; solo el fatalErrorenfoque es correcto.
mate
@matt Correcto, una u otra opción funcionará mejor en diferentes situaciones.
Ben Kane
Correcto, y descubrí y consideré la segunda opción de forma independiente, y a veces tendrá sentido. Por ejemplo, podría haber declarado mi di init(collection:MPMediaItemCollection!). Eso permitiría init(coder:)pasar nada. Pero luego me di cuenta: no, ahora solo estás engañando al compilador. Pasar nulo no es aceptable, así que tira el fatalErrory sigue adelante. :)
mate
1
Sé que esta pregunta y sus respuestas son algo antiguas ahora, pero he publicado una nueva respuesta que aborda algunos puntos que creo que son cruciales para comprender este error que no fue abordado por ninguna de las respuestas existentes.
nhgrif
Buena respuesta. Estoy de acuerdo con usted en que comprender que Swift no siempre hereda los súper inicializadores es esencial para comprender este patrón.
Ben Kane
71

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.

  1. Si un protocolo especifica un inicializador como método requerido, ese inicializador debe marcarse con la requiredpalabra clave de Swift .
  2. Swift tiene un conjunto especial de reglas de herencia con respecto a los initmétodos.

El tl; dr es este:

Si implementa algún inicializador, ya no heredará ninguno de los inicializadores designados de la superclase.

Los únicos inicializadores, si los hay, que heredará, son los inicializadores de conveniencia de súper clase 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 initmétodos.

Sé que este fue el segundo de los dos puntos que hice, pero no podemos entender el primer punto, o por qué la requiredpalabra clave 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 aquí .

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 evita una situación en la que un inicializador simple de una superclase es heredado por una subclase más especializada y se utiliza para crear una nueva instancia de la subclase que no se inicializa de forma completa o correcta.

El énfasis es mío.

Entonces, directamente desde los documentos de Apple, vemos que las subclases Swift no siempre heredarán (y generalmente no) los initmétodos de sus superclases .

Entonces, ¿cuándo heredan de su superclase?

Hay dos reglas que definen cuándo una subclase hereda los initmétodos 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 al heredarlos según la regla 1 o al proporcionar una implementación personalizada como parte de su definición, entonces hereda automáticamente todos los inicializadores de conveniencia de superclase.

Regla 2 no es particularmente relevante a esta conversación porque SKSpriteNode's init(coder: NSCoder)es poco probable que sea un método de conveniencia.

Entonces, su InfoBarclase estaba heredando el requiredinicializador hasta el punto que agregó init(team: Team, size: CGSize).

Si no hubiera proporcionado este initmétodo y, en su lugar, hubiera hecho que InfoBarlas propiedades agregadas de usted fueran opcionales o si les hubiera proporcionado valores predeterminados, entonces todavía habría estado heredando SKSpriteNodelas suyas init(coder: NSCoder). Sin embargo, cuando agregamos nuestro propio inicializador personalizado, dejamos de heredar los inicializadores designados de nuestra superclase (e inicializadores convenientes 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.

ingrese la descripción de la imagen aquí

Si esto fuera Objective-C, no tendría problemas para heredar. Si inicializamos a Barcon initWithFoo:en Objective-C, la self.barpropiedad simplemente sería nil. Es probablemente no es muy bueno, pero es perfectamente válida del estado para el objeto a ser. Es no un estado perfectamente válido para el objeto Swift para estar en. self.barNo es un opcional y no puede ser nil.

Una vez más, la única forma en que heredamos los inicializadores es no proporcionar el nuestro. Entonces, si tratamos de heredar eliminando Bar's 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 initmétodos de superclase :

Problema: la clase 'Bar' no tiene inicializadores

Fix-It: la propiedad 'barra' almacenada sin inicializadores evita los inicializadores sintetizados

Si hemos agregado propiedades almacenadas en nuestra subclase, no hay una manera rápida de crear una instancia válida de nuestra subclase con los inicializadores de la superclase que posiblemente no podrían conocer las propiedades almacenadas de nuestra subclase.


Bien, bien, ¿por qué tengo que implementarlo init(coder: NSCoder)? ¿Por qué es required?

Los initmétodos de Swift pueden jugar con un conjunto especial de reglas de herencia, pero la conformidad del protocolo todavía se hereda en la cadena. Si una clase primaria se ajusta a un protocolo, sus subclases deben cumplir con ese protocolo.

Por lo general, esto no es un problema, porque la mayoría de los protocolos solo requieren métodos que no cumplen con las reglas de herencia especiales en Swift, por lo que si está heredando de una clase que se ajusta a un protocolo, también está heredando todos los métodos o propiedades que permiten que la clase satisfaga la conformidad del protocolo.

Sin embargo, recuerde, los initmétodos de Swift se rigen por un conjunto especial de reglas y no siempre se heredan. Debido a esto, una clase que se ajusta a un protocolo que requiere initmétodos especiales (como NSCoding) requiere que la clase marque esos initmétodos como required.

Considere este ejemplo:

protocol InitProtocol {
    init(foo: Int)
}

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

Esto no se compila. Genera la siguiente advertencia:

Problema: el requisito de inicializador 'init (foo :)' solo puede cumplirse con un inicializador 'requerido' en la clase no final 'ConformingClass'

Fix-It: se requiere insertar

Quiere que haga el init(foo: Int)inicializador requerido. También podría hacerlo feliz haciendo la clase final(lo que significa que no se puede heredar la clase).

Entonces, ¿qué pasa si subclase? Desde este punto, si subclase, estoy bien. Sin embargo, si agrego algunos inicializadores, de repente ya no estoy heredando init(foo:). Esto es problemático porque ahora ya no me estoy conformando con el InitProtocol. No puedo subclase de una clase que se ajusta a un protocolo y luego, de repente, decido que ya no quiero cumplir con ese protocolo. He heredado la conformidad del protocolo, pero debido a la forma en que Swift trabaja con la initherencia del método, no heredé parte de lo que se requiere para cumplir con ese protocolo y debo implementarlo.


Bien, 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 especifica que su clase ya no se ajusta al NSCodingprotocolo heredado y que para solucionarlo debe implementarlo init(coder: NSCoder). Por supuesto.

Pero Xcode simplemente no puede generar ese mensaje porque ese no siempre será el problema real de no implementar o heredar un método requerido. Hay al menos otra razón para hacer initmétodos requiredademás de la conformidad del protocolo, y son los métodos de fábrica.

Si quiero escribir un método de fábrica adecuado, necesito especificar el tipo de retorno que será Self(el equivalente de Swift de Objective-C instanceType). Pero para hacer esto, en realidad necesito usar un requiredmétodo inicializador.

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 de tipo de clase 'Self' con un valor de metatipo debe usar un inicializador 'obligatorio'

ingrese la descripción de la imagen aquí

Es básicamente el mismo problema. Si subclasificamos Box, nuestras subclases heredarán el método de la clase factory. Entonces podríamos llamar SubclassedBox.factory(). Sin embargo, sin la requiredpalabra clave en el init(size:)método, Boxno se garantiza que las subclases hereden la self.init(size:)que factoryestá llamando.

Por lo tanto, debemos hacer ese método requiredsi queremos un método de fábrica como este, y eso significa que si nuestra clase implementa un método como este, tendremos un requiredmétodo de inicialización y nos encontraremos exactamente con los mismos problemas que ha encontrado aquí con el NSCodingprotocolo


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 pueden instanciar su objeto en un estado válido. Pero, por varias razones, una superclase puede marcar un inicializador como required. Cuando lo hace, podemos emplear uno de los escenarios muy específicos por los cuales realmente heredamos el requiredmétodo, o debemos implementarlo nosotros mismos.

Sin embargo, el punto principal aquí es que si recibimos 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 initmétodos de sus padres (lo cual creo que es absolutamente central 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.

ingrese la descripción de la imagen aquí

El mensaje de error que da es un poco engañoso:

Argumento adicional 'b' en llamada

Pero el punto es Barque no hereda ninguno de Foolos initmétodos porque no ha satisfecho ninguno de los dos casos especiales para heredar initmétodos de su clase padre.

Si esto fuera Objective-C, heredaríamos eso initsin 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 servirá. No puede tener un estado no válido, y heredar inicializadores de superclase solo puede conducir a estados de objeto no válidos.

nhgrif
fuente
¿Puede explicar qué significa esta oración o dar un ejemplo? "(e inicializadores convenientes que no apuntaban a los inicializadores que implementamos)"
Abbey Jackson
Brillante respuesta! Deseo que más publicaciones SO sean sobre por qué , como esta, en lugar de solo cómo .
Alexander Vasenin
56

¿Por qué ha surgido este problema? Bueno, el hecho 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 inicializadores que su clase no está preparada para manejar. Los documentos siempre han sido muy claros sobre sus responsabilidades a este respecto. Pero, ¿cuántos de nosotros nos molestamos en cumplirlos, completamente y al pie de la letra? ¡Probablemente ninguno de nosotros! Y el compilador no los hizo cumplir; todo era 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 de elementos de medios reales: la instancia simplemente no puede existir sin una. Pero no he escrito ningún "tapón" para evitar que alguien me inicialice con los huesos descubiertos init. Yo debería haber escrito uno (en realidad, propiamente hablando, debería haber escrito una implementación de initWithNibName:bundle:, el inicializador designado heredada); pero era demasiado vago para molestarme, porque "sabía" que nunca inicializaría incorrectamente mi propia clase de esa manera. Esto dejó un gran agujero. En Objective-C, alguien puede llamar a los huesos init, dejando mis ivars sin inicializar, y estamos arriba del arroyo sin una paleta.

Rápido, maravillosamente, me salva de mí mismo en la mayoría de los casos. Tan pronto como traduje esta aplicación a Swift, todo el problema desapareció. ¡Swift crea efectivamente un tapón para mí! Si init(collection:MPMediaItemCollection)es el único inicializador designado declarado en mi clase, no puedo inicializar llamando a bare-bones init(). ¡Es un milagro!

Lo que sucedió en la semilla 5 es simplemente que el compilador se ha dado 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 una punta, y el compilador no puede evitar eso, y cuando el se init(coder:)cargará la plumilla, se llamará. Entonces el compilador te hace escribir el tapón explícitamente. Y muy bien también.

mate
fuente
Gracias por una respuesta tan detallada. Esto realmente trae luz al asunto.
Julian Osorio
Un voto a favor de pasta12 por decirme cómo hacer que el compilador se calle, pero también un voto a favor por informarme de lo que estaba quejándose en primer lugar.
Garrett Albright
2
Agujero abierto o no, nunca iba a llamar a este init, por lo que es totalmente oficioso obligarme a incluirlo. El código hinchado es una sobrecarga que ninguno de nosotros necesita. Ahora también te obliga a inicializar tus propiedades en ambos inits. ¡Inútil!
Dan Greenfield
55
@DanGreenfield No, no te obliga a inicializar nada, porque si nunca vas a llamarlo, simplemente coloca el fatalErrortapón descrito en stackoverflow.com/a/25128815/341994 . Simplemente conviértalo en un fragmento de código de usuario y, a partir de ahora, puede colocarlo donde sea necesario. Toma medio segundo.
mate
1
@nhgrif Bueno, para ser justos, la pregunta no pedía la historia completa. Se trataba solo de cómo salir de este atasco y seguir adelante. La historia completa se da en mi libro: apeth.com/swiftBook/ch04.html#_class_initializers
mate
33

añadir

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
fuente
3
Esto funciona, pero no creo que sea un error. los inicializadores no se heredan con rapidez (cuando se declara su propio inicializador) y esto se marca con la palabra clave requerida. El único problema es que ahora necesito inicializar TODAS mis propiedades en este método para cada una de mis clases, lo que será una gran cantidad de código desperdiciado, ya que no lo uso en absoluto. O tendré que declarar todas mis propiedades como tipos opcionales implícitamente sin envolver para omitir la inicialización, que tampoco quiero hacer.
Byte épico
1
¡Sip! Me di cuenta tan pronto después de decir que podría ser un error, que en realidad tiene sentido lógico. Estoy de acuerdo en que será un montón de código desperdiciado, ya que como tú nunca usaría este método init. Todavía no estoy seguro de una solución elegante
Gagan Singh
2
Tuve el mismo problema. Tiene sentido con "init requerido", pero swift no es el lenguaje "fácil" que esperaba. Todos estos "opcionales" están haciendo que el lenguaje sea más complejo de lo requerido. Y no hay soporte para DSL y AOP. Me estoy decepcionando cada vez más.
user810395
2
Sí, estoy completamente de acuerdo. Muchas de mis propiedades ahora se declaran como opcionales porque me veo obligado a hacerlo, cuando realmente no se debe permitir que sean nulas. Algunos son opcionales porque legítimamente deberían ser opcionales (lo que significa que nulo ES un valor válido). Y luego, en las clases donde no estoy subclasificando, no necesito usar opciones, por lo que las cosas se están volviendo muy complejas y parece que no puedo encontrar un estilo de codificación correcto. Esperemos que Apple descubra algo.
Byte épico
55
Creo que significan que puede satisfacer el inicializador requerido al no declarar ningún inicializador propio, lo que resultaría en la herencia de todos los inicializadores.
Byte épico