A classe não implementa os membros obrigatórios de sua superclasse

155

Atualizei para o Xcode 6 beta 5 hoje e notei que recebi erros em quase todas as minhas subclasses de classes da Apple.

O erro declara:

A classe 'x' não implementa os membros obrigatórios de sua superclasse

Aqui está um exemplo que eu escolhi, porque essa classe é atualmente bastante leve, por isso será fácil postar.

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)

    }

}

Portanto, minha pergunta é: por que estou recebendo esse erro e como corrigi-lo? O que é que eu não estou implementando? Estou ligando para um inicializador designado.

Byte épico
fonte

Respostas:

127

De um funcionário da Apple nos Fóruns de desenvolvedores:

"Uma maneira de declarar ao compilador e ao programa construído que você realmente não deseja ser compatível com NSCoding é fazer algo assim:"

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

Se você sabe que não deseja ser compatível com NSCoding, esta é uma opção. Adotei essa abordagem com grande parte do meu código SpriteKit, pois sei que não a carregarei de um storyboard.


Outra opção que você pode usar e que funciona muito bem é implementar o método como um init de conveniência, assim:

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

Observe a chamada para um inicializador em self. Isso permite que você precise usar apenas valores fictícios para os parâmetros, em oposição a todas as propriedades não opcionais, evitando gerar um erro fatal.


A terceira opção, é claro, é implementar o método enquanto chama super e inicializar todas as suas propriedades não opcionais. Você deve adotar essa abordagem se o objeto for uma visualização sendo carregada de um storyboard:

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

    super.init(coder: aDecoder)
}
Ben Kane
fonte
3
A segunda opção é inútil na maioria dos casos da vida real. Tomemos, por exemplo, meu inicializador necessário init(collection:MPMediaItemCollection). Você deve fornecer uma coleção real de itens de mídia; esse é o ponto desta classe. Esta classe simplesmente não pode ser instanciada sem uma. Ele vai analisar a coleção e inicializar uma dúzia de variáveis ​​de instância. Esse é o ponto principal deste ser o único inicializador designado! Portanto, init(coder:)não tem MPMediaItemCollection significativo (ou mesmo sem sentido) para fornecer aqui; apenas a fatalErrorabordagem está certa.
Matt
@matt Correto, uma ou outra opção funcionará melhor em diferentes situações.
Ben Kane
Certo, e eu descobri e considerei a segunda opção de forma independente, e às vezes faz sentido. Por exemplo, eu poderia ter declarado meu di init(collection:MPMediaItemCollection!). Isso permitiria init(coder:)passar nulo. Mas então eu percebi: não, agora você está apenas enganando o compilador. Passar nulo não é aceitável, então jogue o fatalErrore siga em frente. :)
mate
1
Eu sei que essa pergunta e suas respostas são meio antigas agora, mas eu publiquei uma nova resposta que aborda alguns pontos que considero cruciais para realmente entender esse erro que não foi abordado por nenhuma das respostas existentes.
Nhgrif 20/08/2015
Boa resposta. Concordo que o entendimento de que o Swift nem sempre herda superinicializadores é essencial para entender esse padrão.
Ben Kane
71

Existem duas partes absolutamente cruciais de informações específicas do Swift que estão faltando nas respostas existentes e acho que ajudam a esclarecer isso completamente.

  1. Se um protocolo especificar um inicializador como um método necessário, esse inicializador deverá ser marcado usando a requiredpalavra-chave Swift .
  2. Swift tem um conjunto especial de regras de herança em relação a initmétodos.

O tl; dr é o seguinte:

Se você implementar qualquer inicializador, não estará mais herdando nenhum dos inicializadores designados da superclasse.

Os únicos inicializadores, se houver, que você herdará, são inicializadores de conveniência de super classe que apontam para um inicializador designado que você substituiu.

Então ... pronto para a versão longa?


Swift tem um conjunto especial de regras de herança em relação a initmétodos.

Sei que esse foi o segundo de dois argumentos que fiz, mas não conseguimos entender o primeiro, ou por que a requiredpalavra - chave existe até entendermos esse ponto. Uma vez que entendemos esse ponto, o outro se torna bastante óbvio.

Todas as informações que abordo nesta seção desta resposta são da documentação da Apple encontrada aqui .

Dos documentos da Apple:

Diferentemente das subclasses no Objective-C, as subclasses Swift não herdam seus inicializadores de superclasse por padrão. A abordagem de Swift evita uma situação em que um inicializador simples de uma superclasse é herdado por uma subclasse mais especializada e é usado para criar uma nova instância da subclasse que não é inicializada total ou corretamente.

Ênfase minha.

Portanto, diretamente dos documentos da Apple, vemos que as subclasses Swift nem sempre (e geralmente não) herdam os initmétodos de suas superclasses .

Então, quando eles herdam da superclasse?

Existem duas regras que definem quando uma subclasse herda initmétodos de seu pai. Dos documentos da Apple:

Regra 1

Se sua subclasse não definir nenhum inicializador designado, ela herdará automaticamente todos os inicializadores designados da superclasse.

Regra 2

Se sua subclasse fornecer uma implementação de todos os inicializadores designados para sua superclasse - herdando-os de acordo com a regra 1 ou fornecendo uma implementação customizada como parte de sua definição -, ela herdará automaticamente todos os inicializadores de conveniência da superclasse.

Regra 2 não é particularmente relevante para esta conversa, porque SKSpriteNode's init(coder: NSCoder)é improvável que seja um método de conveniência.

Portanto, sua InfoBarclasse estava herdando o requiredinicializador até o ponto que você adicionou init(team: Team, size: CGSize).

Se você não forneceu esse initmétodo e, em vez disso, tornou InfoBaropcional as propriedades adicionadas, ou forneceu valores padrão, você ainda estaria herdando SKSpriteNodeas init(coder: NSCoder). No entanto, quando adicionamos nosso próprio inicializador personalizado, paramos de herdar os inicializadores designados da superclasse (e inicializadores de conveniência que não apontam para os inicializadores que implementamos).

Então, como um exemplo simplista, apresento o seguinte:

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 apresenta o seguinte erro:

Argumento ausente para o parâmetro 'bar' na chamada.

insira a descrição da imagem aqui

Se fosse o Objective-C, não haveria problema em herdar. Se inicializamos a Barcom initWithFoo:em Objective-C, a self.barpropriedade seria simplesmente nil. Provavelmente não é grande, mas é um perfeitamente válido estado para o objeto a ser. É não um estado perfeitamente válido para o objeto Swift estar dentro. self.barNão é um opcional e não pode ser nil.

Novamente, a única maneira de herdar inicializadores é não fornecer os nossos. Portanto, se tentarmos herdar excluindo Bar's init(foo: String, bar: String), como tal:

class Bar: Foo {
    var bar: String
}

Agora voltamos a herdar (mais ou menos), mas isso não será compilado ... e a mensagem de erro explica exatamente por que não herdamos os initmétodos da superclasse :

Problema: a classe 'Bar' não possui inicializadores

Fix-It: Propriedade armazenada 'bar' sem inicializadores impede inicializadores sintetizados

Se adicionamos propriedades armazenadas em nossa subclasse, não há maneira Swift possível de criar uma instância válida de nossa subclasse com os inicializadores de superclasse que não poderiam saber sobre as propriedades armazenadas de nossa subclasse.


Ok, bem, por que eu tenho que implementar init(coder: NSCoder)? Por que é isso required?

Os initmétodos de Swift podem desempenhar um conjunto especial de regras de herança, mas a conformidade com o protocolo ainda é herdada na cadeia. Se uma classe pai está em conformidade com um protocolo, suas subclasses devem estar em conformidade com esse protocolo.

Normalmente, isso não é um problema, porque a maioria dos protocolos exige apenas métodos que não são executados por regras de herança especiais no Swift, portanto, se você está herdando de uma classe que está em conformidade com um protocolo, também está herdando todos os métodos ou propriedades que permitem à classe satisfazer a conformidade do protocolo.

No entanto, lembre-se, os initmétodos de Swift seguem um conjunto especial de regras e nem sempre são herdados. Por esse motivo, uma classe que está em conformidade com um protocolo que requer initmétodos especiais (como NSCoding) exige que a classe marque esses initmétodos como required.

Considere este exemplo:

protocol InitProtocol {
    init(foo: Int)
}

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

Isso não compila. Ele gera o seguinte aviso:

Problema: o requisito do inicializador 'init (foo :)' pode ser atendido apenas por um inicializador 'obrigatório' na classe não final 'ConformingClass'

Fix-It: inserção necessária

Ele quer que eu faça o init(foo: Int)inicializador necessário. Eu também poderia torná-lo feliz fazendo a classe final(o que significa que a classe não pode ser herdada).

Então, o que acontece se eu subclasse? A partir deste ponto, se eu subclasse, estou bem. Se eu adicionar inicializadores, de repente, não estou mais herdando init(foo:). Isso é problemático porque agora não estou mais em conformidade com o InitProtocol. Não posso subclassificar de uma classe que esteja em conformidade com um protocolo e, de repente, decidir que não quero mais estar em conformidade com esse protocolo. Eu herdei a conformidade do protocolo, mas devido à maneira como o Swift trabalha com a initherança de métodos, não herdei parte do necessário para estar em conformidade com esse protocolo e preciso implementá-lo.


Ok, tudo isso faz sentido. Mas por que não consigo receber uma mensagem de erro mais útil?

Indiscutivelmente, a mensagem de erro pode ser mais clara ou melhor se especificar que sua classe não está mais em conformidade com o NSCodingprotocolo herdado e que, para corrigi-lo, é necessário implementar init(coder: NSCoder). Certo.

Mas o Xcode simplesmente não pode gerar essa mensagem, porque esse nem sempre será o problema real de não implementar ou herdar um método necessário. Há pelo menos um outro motivo para criar initmétodos requiredalém da conformidade com o protocolo, e esses são métodos de fábrica.

Se eu quiser escrever um método de fábrica adequado, preciso especificar o tipo de retorno a ser Self(o equivalente de Swift aos Objective-C instanceType). Mas, para fazer isso, eu realmente preciso usar um requiredmétodo inicializador.

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

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

Isso gera o erro:

A construção de um objeto do tipo de classe 'Self' com um valor de metatype deve usar um inicializador 'required'

insira a descrição da imagem aqui

É basicamente o mesmo problema. Se subclassificarmos Box, nossas subclasses herdarão o método de classe factory. Para que pudéssemos ligar SubclassedBox.factory(). No entanto, sem a requiredpalavra-chave no init(size:)método, Boxnão é garantido que as subclasses de herdem o self.init(size:)que factoryestá chamando.

Portanto, precisamos criar esse método requiredse quisermos um método de fábrica como este, e isso significa que, se nossa classe implementar um método como este, teremos um requiredmétodo inicializador e encontraremos exatamente os mesmos problemas que você encontrou aqui com o NSCodingprotocolo.


Por fim, tudo se resume ao entendimento básico de que os inicializadores do Swift jogam com um conjunto de regras de herança um pouco diferente, o que significa que você não tem a garantia de herdar inicializadores da sua superclasse. Isso acontece porque os inicializadores de superclasse não podem conhecer suas novas propriedades armazenadas e não conseguiram instanciar seu objeto em um estado válido. Mas, por várias razões, uma superclasse pode marcar um inicializador como required. Quando isso acontece, podemos empregar um dos cenários muito específicos pelos quais realmente herdamos o requiredmétodo ou devemos implementá-lo nós mesmos.

O ponto principal aqui é que, se estamos recebendo o erro que você vê aqui, significa que sua classe não está realmente implementando o método.

Como talvez um exemplo final para aprofundar o fato de que as subclasses Swift nem sempre herdam os initmétodos de seus pais (o que eu acho absolutamente essencial para entender completamente esse problema), considere este exemplo:

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)

Isso falha ao compilar.

insira a descrição da imagem aqui

A mensagem de erro exibida é um pouco enganadora:

Argumento extra 'b' na chamada

Mas o ponto é, Barnão herda qualquer um dos Foo's initmétodos porque não satisfez nenhum dos dois casos especiais para herdar initmétodos de sua classe pai.

Se fosse o Objective-C, herdaríamos isso initsem problemas, porque o Objective-C é perfeitamente feliz por não inicializar as propriedades dos objetos (embora, como desenvolvedor, você não devesse estar satisfeito com isso). Em Swift, isso simplesmente não funciona. Você não pode ter um estado inválido e a herança de inicializadores de superclasse pode levar apenas a estados de objetos inválidos.

nhgrif
fonte
Você pode explicar o que essa frase significa ou dar um exemplo? "(e initializers de conveniência que não apontam para initializers que implementamos)"
Abbey Jackson
Resposta brilhante! Desejo que mais posts do SO sejam sobre o porquê , como este, em vez de apenas como .
Alexander Vasenin 19/10/10
56

Por que esse problema surgiu? Bem, o fato claro é que sempre foi importante (ou seja, no Objective-C, desde o dia em que comecei a programar o Cocoa no Mac OS X 10.0) para lidar com inicializadores que sua classe não está preparada para lidar. Os documentos sempre foram bastante claros sobre suas responsabilidades nesse sentido. Mas quantos de nós se preocuparam em cumpri-las, completamente e de acordo com a letra? Provavelmente nenhum de nós! E o compilador não os aplicou; tudo era puramente convencional.

Por exemplo, na minha subclasse de controlador Objective-C view com este inicializador designado:

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

... é crucial que recebamos uma coleção real de itens de mídia: a instância simplesmente não pode existir sem uma. Mas não escrevi nenhuma "rolha" para impedir que alguém me inicializasse com os ossos init. Eu deveria ter escrito um (na verdade, falando corretamente, eu deveria ter escrito uma implementação do initWithNibName:bundle:inicializador designado herdado); mas eu estava com preguiça de me preocupar, porque "sabia" que nunca inicializaria incorretamente minha própria classe dessa maneira. Isso deixou um buraco. No Objetivo-C, alguém pode chamar de initesqueleto, deixando meus ivars não inicializados, e nós estamos subindo o riacho sem remo.

Swift, maravilhosamente, me salva de mim mesmo na maioria dos casos. Assim que traduzi este aplicativo para o Swift, o problema foi resolvido. Swift efetivamente cria uma rolha para mim! Se init(collection:MPMediaItemCollection)é o único inicializador designado declarado na minha classe, não posso ser inicializado chamando bare-bones init(). É um milagre!

O que aconteceu na semente 5 é apenas que o compilador percebeu que o milagre não funciona no caso de init(coder:), porque, em teoria, uma instância dessa classe pode vir de uma ponta, e o compilador não pode impedir isso - e quando o cargas de ponta, init(coder:)serão chamadas. Portanto, o compilador faz você escrever a rolha explicitamente. E muito certo também.

mate
fonte
Obrigado por essa resposta detalhada. Isso realmente traz luz ao assunto.
Julian Osorio
Um voto positivo ao macarrão12 por me dizer como fazer o compilador calar a boca, mas um voto positivo a você também por me instruir sobre o que ele estava reclamando em primeiro lugar.
Garrett Albright
2
Escancarado ou não, eu nunca chamaria isso de init, por isso é totalmente ofensivo me forçar a incluí-lo. O código inchado é uma sobrecarga que nenhum de nós precisa. Agora também obriga a inicializar suas propriedades em ambas as unidades. Sem sentido!
Dan Greenfield
5
@ DanGreenfield Não, isso não força você a inicializar nada, porque se você nunca vai chamá-lo, basta colocar a fatalErrorrolha descrita em stackoverflow.com/a/25128815/341994 . Basta torná-lo um snippet de código do usuário e, a partir de agora, você pode inseri-lo onde for necessário. Leva meio segundo.
Matt
1
@ nhgrif Bem, para ser justo, a pergunta não pediu a história completa. Era exatamente como sair dessa confusão e seguir em frente. A história completa é apresentada no meu livro: apeth.com/swiftBook/ch04.html#_class_initializers
mate
33

adicionar

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}
Gagan Singh
fonte
3
Isso funciona, mas não acho que seja um bug. inicializadores não são herdados em swift (quando seu próprio inicializador é declarado) e isso é marcado com a palavra-chave necessária. O único problema é que agora eu preciso inicializar TODAS as minhas propriedades neste método para cada uma das minhas classes, que serão um monte de código desperdiçado, pois eu não uso nada disso. Ou terei que declarar todas as minhas propriedades como tipos opcionais implicitamente desembrulhados para ignorar a inicialização que eu também não quero fazer.
Epic Byte
1
Sim! Percebi, logo depois de dizer que poderia ser um bug, que realmente faz sentido lógico. Concordo que haverá muito código desperdiçado, pois como você, nunca usaria esse método init. Ainda não tenho certeza de uma solução elegante
Gagan Singh
2
Eu tive o mesmo problema. Faz sentido com o "init necessário", mas swift não é a linguagem "fácil" que eu esperava. Todos esses "opcionais" estão tornando o idioma mais complexo do que o necessário. E nenhum suporte para DSL e AOP. Estou ficando cada vez mais decepcionado.
precisa saber é o seguinte
2
Sim, eu concordo completamente. Muitas das minhas propriedades agora são declaradas como opcionais porque sou forçado a fazê-lo, quando na verdade elas não deveriam ser nulas. Alguns são opcionais porque legitimamente devem ser opcionais (o que significa que NIL É um valor válido). E então, nas aulas em que não estou subclassificando, não preciso usar opcionais; portanto, as coisas estão ficando muito complexas e não consigo encontrar o estilo certo de codificação. Felizmente, a Apple descobre algo.
Byte épico
5
Eu acho que eles significam que você pode satisfazer o inicializador necessário, não declarando nenhum inicializador, o que resultaria na herança de todos os inicializadores.
Epic Byte