La classe non implementa i membri richiesti della sua superclasse


155

Così ho aggiornato Xcode 6 beta 5 oggi e ho notato che ho ricevuto errori in quasi tutte le mie sottoclassi delle classi Apple.

L'errore indica:

La classe 'x' non implementa i membri richiesti della sua superclasse

Ecco un esempio che ho scelto perché questa classe è attualmente piuttosto leggera, quindi sarà facile pubblicare.

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)

    }

}

Quindi la mia domanda è: perché ricevo questo errore e come posso risolverlo? Cosa non sto implementando? Chiamo un inizializzatore designato.

Risposte:


127

Da un dipendente Apple nei forum degli sviluppatori:

"Un modo per dichiarare al compilatore e al programma creato che in realtà non vuoi essere compatibile con NSCoding è fare qualcosa del genere:"

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

Se sai di non voler essere conforme a NSCoding, questa è un'opzione. Ho adottato questo approccio con molto del mio codice SpriteKit, poiché so che non lo caricherò da uno storyboard.


Un'altra opzione che puoi prendere, che funziona piuttosto bene, è implementare il metodo come init convenienza, in questo modo:

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

Nota la chiamata a un inizializzatore in self. Ciò consente di utilizzare solo valori fittizi per i parametri, al contrario di tutte le proprietà non opzionali, evitando al contempo di generare un errore fatale.


La terza opzione ovviamente è quella di implementare il metodo mentre si chiama super e inizializzare tutte le proprietà non opzionali. Dovresti adottare questo approccio se l'oggetto è una vista caricata da uno storyboard:

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

    super.init(coder: aDecoder)
}

3
La seconda opzione è inutile nella maggior parte dei casi della vita reale, però. Prendi, ad esempio, il mio inizializzatore richiesto init(collection:MPMediaItemCollection). È necessario fornire una raccolta di articoli multimediali reali; questo è il punto di questa classe. Questa classe semplicemente non può essere istanziata senza una. Analizzerà la raccolta e inizializzerà una dozzina di variabili di istanza. Questo è il punto centrale di questo essere l'unico inizializzatore designato! Pertanto, init(coder:)non ha alcun MPMediaItemCollection significativo (o addirittura insignificante) da fornire qui; solo l' fatalErrorapproccio è giusto.
matt

@matt Corretto, l'una o l'altra opzione funzionerà meglio in diverse situazioni.
Ben Kane,

Giusto, e ho scoperto e considerato la seconda opzione in modo indipendente, e talvolta avrà senso. Ad esempio avrei potuto dichiarare il mio di init(collection:MPMediaItemCollection!). Ciò consentirebbe init(coder:)di passare a zero. Ma poi ho capito: no, ora stai solo prendendo in giro il compilatore. Passare lo zero non è accettabile, quindi lancia fatalErrore vai avanti. :)
matt

1
Conosco questa domanda e le sue risposte sono un po 'vecchie ora, ma ho pubblicato una nuova risposta che affronta alcuni punti che penso siano cruciali per comprendere effettivamente questo errore che non sono stati affrontati da nessuna delle risposte esistenti.
nhgrif,

Buona risposta. Sono d'accordo con te sul fatto che capire che Swift non eredita sempre i superinizializzatori è essenziale per comprendere questo schema.
Ben Kane,

71

Ci sono due pezzi assolutamente cruciali di informazioni specifiche su Swift che mancano nelle risposte esistenti che penso aiutino a chiarire completamente.

  1. Se un protocollo specifica un inizializzatore come metodo richiesto, tale inizializzatore deve essere contrassegnato utilizzando la requiredparola chiave Swift .
  2. Swift ha un set speciale di regole di ereditarietà relative ai initmetodi.

Il tl; dr è questo:

Se si implementano inizializzatori, non si eredita più nessuno degli inizializzatori designati della superclasse.

Gli unici inizializzatori, se presenti, che erediterai, sono inizializzatori di convenienza di classe superiore che puntano a un inizializzatore designato che ti è capitato di ignorare.

Quindi ... pronto per la versione lunga?


Swift ha un set speciale di regole di ereditarietà relative ai initmetodi.

So che questo è stato il secondo dei due punti che ho fatto, ma non possiamo capire il primo punto, o perché la requiredparola chiave esiste anche fino a quando non capiamo questo punto. Una volta compreso questo punto, l'altro diventa piuttosto ovvio.

Tutte le informazioni che copro in questa sezione di questa risposta provengono dalla documentazione di Apple trovata qui .

Dai documenti Apple:

A differenza delle sottoclassi in Objective-C, le sottoclassi Swift non ereditano i loro inizializzatori di superclasse per impostazione predefinita. L'approccio di Swift evita una situazione in cui un semplice inizializzatore da una superclasse viene ereditato da una sottoclasse più specializzata e viene utilizzato per creare una nuova istanza della sottoclasse che non è inizializzata completamente o correttamente.

Enfasi mia.

Quindi, direttamente dai documenti di Apple proprio lì, vediamo che le sottoclassi di Swift non erediteranno sempre (e di solito non lo fanno) i initmetodi della loro superclasse .

Quindi, quando ereditano dalla loro superclasse?

Esistono due regole che definiscono quando una sottoclasse eredita i initmetodi dal suo genitore. Dai documenti di Apple:

Regola 1

Se la sottoclasse non definisce alcun inizializzatore designato, eredita automaticamente tutti i suoi inizializzatori designati superclasse.

Regola 2

Se la sottoclasse fornisce un'implementazione di tutti i suoi inizializzatori designati della superclasse, ereditandoli secondo la regola 1 o fornendo un'implementazione personalizzata come parte della sua definizione, eredita automaticamente tutti gli inizializzatori di convenienza della superclasse.

Regola 2 non è particolarmente rilevante per questa conversazione perché SKSpriteNodes' init(coder: NSCoder)è improbabile che sia un metodo comodo.

Quindi, la tua InfoBarclasse stava ereditando l' requiredinizializzatore fino al punto che hai aggiunto init(team: Team, size: CGSize).

Se si dovesse non hanno fornito questo initmetodo e invece effettuato le InfoBar'proprietà s aggiunto opzionale o fornito loro i valori di default, allora avresti ancora stato eredita SKSpriteNode' s init(coder: NSCoder). Tuttavia, quando abbiamo aggiunto il nostro inizializzatore personalizzato, abbiamo smesso di ereditare gli inizializzatori designati della nostra superclasse (e inizializzatori di convenienza che non puntavano agli inizializzatori che abbiamo implementato).

Quindi, come esempio semplicistico, vi presento questo:

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")

Che presenta il seguente errore:

Argomento mancante per il parametro 'bar' nella chiamata.

inserisci qui la descrizione dell'immagine

Se questo fosse Objective-C, non avrebbe problemi a ereditare. Se inizializzassimo aBar con initWithFoo:in Objective-C, la self.barproprietà sarebbe semplicemente nil. Probabilmente non è grande, ma è perfettamente valida di stato per l'oggetto deve essere. E ' non è uno stato perfettamente valida per l'oggetto Swift di essere in. self.barNon è un optional e non può essere nil.

Ancora una volta, l'unico modo in cui ereditiamo gli inizializzatori è non fornire i nostri. Quindi, se cerchiamo di ereditare cancellando Bar's init(foo: String, bar: String), come ad esempio:

class Bar: Foo {
    var bar: String
}

Ora siamo tornati a ereditare (una specie di), ma questo non si compila ... e il messaggio di errore spiega esattamente perché non ereditiamo i initmetodi di superclasse :

Problema: la classe "Bar" non ha inizializzatori

Fix-It: la 'barra' delle proprietà memorizzata senza inizializzatori impedisce gli inizializzatori sintetizzati

Se abbiamo aggiunto proprietà memorizzate nella nostra sottoclasse, non esiste alcun modo rapido per creare un'istanza valida della nostra sottoclasse con gli inizializzatori della superclasse che non potrebbero conoscere le proprietà memorizzate della nostra sottoclasse.


Va bene, perché devo implementare init(coder: NSCoder) ? Perché è required?

I initmetodi di Swift possono svolgere un set speciale di regole di ereditarietà, ma la conformità del protocollo è ancora ereditata lungo la catena. Se una classe genitore è conforme a un protocollo, le sue sottoclassi devono essere conformi a quel protocollo.

Di solito, questo non è un problema, poiché la maggior parte dei protocolli richiede solo metodi che non giocano con regole di ereditarietà speciali in Swift, quindi se erediti da una classe conforme a un protocollo, stai anche ereditando tutti i metodi o proprietà che consentono alla classe di soddisfare la conformità del protocollo.

Tuttavia, ricorda, i initmetodi di Swift si basano su un insieme speciale di regole e non sono sempre ereditati. Per questo motivo, una classe conforme a un protocollo che richiede initmetodi speciali (come NSCoding) richiede che la classe contrassegni tali initmetodi come required.

Considera questo esempio:

protocol InitProtocol {
    init(foo: Int)
}

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

Questo non si compila. Genera il seguente avviso:

Problema: il requisito di inizializzazione 'init (foo :)' può essere soddisfatto solo da un inizializzatore 'obbligatorio' nella classe non finale 'ConformingClass'

Fix-It: inserire richiesto

Vuole che renda init(foo: Int)necessario l' inizializzatore. Potrei anche renderlo felice creando la classe final(il che significa che la classe non può essere ereditata).

Quindi, cosa succede se eseguo una sottoclasse? Da questo punto, se faccio una sottoclasse, sto bene. Se aggiungo comunque degli inizializzatori, all'improvviso non eredito più init(foo:). Questo è problematico perché ora non sono più conforme a InitProtocol. Non posso effettuare la sottoclasse da una classe conforme a un protocollo e poi improvvisamente decidere di non voler più conformarmi a quel protocollo. Ho ereditato la conformità del protocollo, ma a causa del modo in cui Swift lavora con l' initereditarietà del metodo, non ho ereditato parte di ciò che è necessario per conformarsi a quel protocollo e devo implementarlo.


Va bene, tutto questo ha senso. Ma perché non riesco a visualizzare un messaggio di errore più utile?

Probabilmente, il messaggio di errore potrebbe essere più chiaro o migliore se specificasse che la classe non era più conforme al NSCodingprotocollo ereditato e che per risolverlo è necessario implementare init(coder: NSCoder). Sicuro.

Ma Xcode semplicemente non può generare quel messaggio perché in realtà non sarà sempre il vero problema con la mancata implementazione o ereditarietà di un metodo richiesto. C'è almeno un altro motivo per rendere i initmetodi requiredoltre alla conformità del protocollo, e questo è metodi di fabbrica.

Se voglio scrivere un metodo factory adeguato, devo specificare il tipo restituito Self(equivalente di Swift di Objective-C instanceType). Ma per fare questo, ho davvero bisogno di usare un requiredmetodo di inizializzazione.

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

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

Questo genera l'errore:

La costruzione di un oggetto del tipo di classe "Self" con un valore di metatype deve utilizzare un inizializzatore "obbligatorio"

inserisci qui la descrizione dell'immagine

È sostanzialmente lo stesso problema. Se effettuiamo la sottoclasse Box, le nostre sottoclassi erediteranno il metodo class factory. Quindi potremmo chiamare SubclassedBox.factory(). Tuttavia, senza la requiredparola chiave sul init(size:)metodo, Boxle sottoclassi del metodo non sono garantite per ereditare self.init(size:)ciò che factorysta chiamando.

Quindi dobbiamo fare quel metodo requiredse vogliamo un metodo factory come questo, e ciò significa che se la nostra classe implementa un metodo come questo, avremo un requiredmetodo di inizializzazione e incontreremo gli stessi identici problemi che hai incontrato qui con il NSCodingprotocollo.


Alla fine, tutto si riduce alla comprensione di base che gli inizializzatori di Swift giocano secondo un insieme leggermente diverso di regole di ereditarietà, il che significa che non è garantito che tu erediti gli inizializzatori dalla tua superclasse. Ciò accade perché gli inizializzatori di superclasse non possono conoscere le nuove proprietà archiviate e non sono in grado di creare un'istanza dell'oggetto in uno stato valido. Ma, per vari motivi, una superclasse potrebbe contrassegnare un inizializzatore come required. In tal caso, possiamo utilizzare uno degli scenari molto specifici con cui ereditiamo effettivamente il requiredmetodo o dobbiamo implementarlo da soli.

Il punto principale qui è che se stiamo riscontrando l'errore che vedi qui, significa che la tua classe non sta implementando il metodo.

Come forse un ultimo esempio per approfondire il fatto che le sottoclassi di Swift non ereditano sempre i initmetodi dei loro genitori (che penso sia assolutamente centrale per comprendere appieno questo problema), considera questo esempio:

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)

Questo non riesce a compilare.

inserisci qui la descrizione dell'immagine

Il messaggio di errore che fornisce è un po 'fuorviante:

Argomento aggiuntivo "b" in chiamata

Ma il punto è, Barnon eredita nessuna delle Foo's initmetodi, perché non ha soddisfatto nessuno dei due casi particolari per ereditare initi metodi dalla sua classe padre.

Se questo fosse Objective-C, lo erediteremmo initsenza problemi, perché Objective-C è perfettamente felice di non inizializzare le proprietà degli oggetti (anche se come sviluppatore, non avresti dovuto esserlo). In Swift, questo semplicemente non lo farà. Non è possibile avere uno stato non valido ed ereditare gli inizializzatori di superclasse può solo portare a stati di oggetto non validi.


Puoi spiegare cosa significa questa frase o fare un esempio? "(e inizializzatori di convenienza che non indicano gli inizializzatori che abbiamo implementato)"
Abbey Jackson,

Risposta brillante! Vorrei che più post SO riguardassero il perché , come questo, anziché solo come .
Alexander Vasenin,

56

Perché è sorto questo problema? Bene, il fatto evidente è che è sempre stato importante (cioè in Objective-C, dal giorno in cui ho iniziato a programmare Cocoa in Mac OS X 10.0) per gestire gli inizializzatori che la tua classe non è pronta a gestire. I documenti sono sempre stati abbastanza chiari sulle tue responsabilità al riguardo. Ma quanti di noi si sono presi la briga di soddisfarli, completamente e alla lettera? Probabilmente nessuno di noi! E il compilatore non li ha fatti rispettare; era tutto puramente convenzionale.

Ad esempio, nella mia sottoclasse del controller della vista Objective-C con questo inizializzatore designato:

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

... è fondamentale che ci sia passata una vera raccolta di articoli multimediali: l'istanza semplicemente non può nascere senza uno. Ma non ho scritto alcun "tappo" per impedire a qualcuno di inizializzarmi a ossa nude init. Ho dovuto scritto uno (in realtà, propriamente parlando, avrei dovuto scrivere un'implementazione initWithNibName:bundle:, l'inizializzatore designato ereditato); ma ero troppo pigro per disturbare, perché "sapevo" che non avrei mai inizializzato in modo errato la mia classe in quel modo. Ciò ha lasciato un buco spalancato. In Objective-C, qualcuno può chiamare ossa nude init, lasciando i miei avari non inizializzati, e siamo su per il torrente senza pagaia.

Rapido, meravigliosamente, mi salva da me stesso nella maggior parte dei casi. Non appena ho tradotto questa app in Swift, l'intero problema è scomparso. Swift crea efficacemente un tappo per me! Se init(collection:MPMediaItemCollection)è l'unico inizializzatore designato dichiarato nella mia classe, non posso essere inizializzato chiamando bare-bones init(). È un miracolo!

Quello che è successo nel seed 5 è semplicemente che il compilatore si è reso conto che il miracolo non funziona nel caso di init(coder:), perché in teoria un'istanza di questa classe potrebbe provenire da un pennino e il compilatore non può impedirlo - e quando il il pennino init(coder:)verrà chiamato. Quindi il compilatore ti fa scrivere esplicitamente il tappo. E anche abbastanza bene.


Grazie per una risposta così dettagliata. Questo porta davvero luce sulla questione.
Julian Osorio,

Un voto a pasta12 per avermi detto come far tacere il compilatore, ma anche un voto a te per avermi fatto capire cosa stava piangendo in primo luogo.
Garrett Albright,

2
Con il buco o meno, non avrei mai chiamato questo init, quindi è del tutto ufficiale costringermi a includerlo. Il codice gonfiato è un sovraccarico di cui nessuno di noi ha bisogno. Inoltre ora ti costringe a inizializzare le tue proprietà anche in entrambi gli init. Inutile!
Dan Greenfield,

5
@DanGreenfield No, non ti obbliga a inizializzare nulla, perché se non lo chiamerai mai, inserisci semplicemente il fatalErrortappo descritto in stackoverflow.com/a/25128815/341994 . Basta trasformarlo in un frammento di codice utente e d'ora in poi puoi semplicemente inserirlo nel punto in cui è necessario. Ci vuole mezzo secondo.
matt

1
@nhgrif Beh, per essere onesti, la domanda non ha posto la storia a sfondo intero. Si trattava solo di come uscire da questa marmellata e andare avanti. L'intera storia è riportata nel mio libro: apeth.com/swiftBook/ch04.html#_class_initializers
matt

33

Inserisci

required init(coder aDecoder: NSCoder!) {
  super.init(coder: aDecoder)
}

3
Funziona, ma non credo sia un bug. gli inizializzatori non vengono ereditati in rapido (quando viene dichiarato il proprio inizializzatore) e questo è contrassegnato con la parola chiave richiesta. L'unico problema è che ora ho bisogno di inizializzare TUTTE le mie proprietà in questo metodo per ognuna delle mie classi, che sarà un sacco di codice sprecato perché non lo uso affatto. O dovrò dichiarare tutte le mie proprietà come tipi opzionali implicitamente da scartare per bypassare l'inizializzazione che anch'io non voglio fare.
Epic Byte,

1
Sì! Mi sono reso conto appena dopo aver detto che potrebbe essere un bug, che in realtà ha un senso logico. Sono d'accordo che sarà un sacco di codice sprecato, poiché come te non userei mai questo metodo init. Non sono ancora sicuro di una soluzione elegante
Gagan Singh,

2
Ho avuto lo stesso problema. Ha senso con "init richiesto", ma rapido non è il linguaggio "facile" che speravo. Tutti questi "opzionali" stanno rendendo la lingua più complessa del necessario. E nessun supporto per DSL e AOP. Sto diventando sempre più deluso.
user810395

2
Sì, sono completamente d'accordo. Molte delle mie proprietà sono ora dichiarate come opzionali perché sono costretto a farlo, quando in realtà non si dovrebbe permettere loro di essere nulle. Alcuni sono opzionali perché legittimamente dovrebbero essere opzionali (nel senso che zero è un valore valido). E poi nelle classi in cui non faccio la sottoclasse non ho bisogno di usare gli opzionali, quindi le cose stanno diventando molto complesse e non riesco a trovare il giusto stile di codifica. Speriamo che Apple capisca qualcosa.
Epic Byte,

5
Penso che significhino che puoi soddisfare l'inizializzatore richiesto non dichiarando alcun tuo inizializzatore, il che comporterebbe l'ereditarietà di tutti gli inizializzatori.
Epic Byte,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.