Ci sono due pezzi assolutamente cruciali di informazioni specifiche su Swift che mancano nelle risposte esistenti che penso aiutino a chiarire completamente.
- Se un protocollo specifica un inizializzatore come metodo richiesto, tale inizializzatore deve essere contrassegnato utilizzando la
required
parola chiave Swift .
- Swift ha un set speciale di regole di ereditarietà relative ai
init
metodi.
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 init
metodi.
So che questo è stato il secondo dei due punti che ho fatto, ma non possiamo capire il primo punto, o perché la required
parola 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 init
metodi della loro superclasse .
Quindi, quando ereditano dalla loro superclasse?
Esistono due regole che definiscono quando una sottoclasse eredita i init
metodi 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é SKSpriteNode
s' init(coder: NSCoder)
è improbabile che sia un metodo comodo.
Quindi, la tua InfoBar
classe stava ereditando l' required
inizializzatore fino al punto che hai aggiunto init(team: Team, size: CGSize)
.
Se si dovesse non hanno fornito questo init
metodo 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.
Se questo fosse Objective-C, non avrebbe problemi a ereditare. Se inizializzassimo aBar
con initWithFoo:
in Objective-C, la self.bar
proprietà 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.bar
Non è 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 init
metodi 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 init
metodi 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 init
metodi 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 init
metodi speciali (come NSCoding
) richiede che la classe contrassegni tali init
metodi 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' init
ereditarietà 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 NSCoding
protocollo 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 init
metodi required
oltre 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 required
metodo 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"
È 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 required
parola chiave sul init(size:)
metodo, Box
le sottoclassi del metodo non sono garantite per ereditare self.init(size:)
ciò che factory
sta chiamando.
Quindi dobbiamo fare quel metodo required
se vogliamo un metodo factory come questo, e ciò significa che se la nostra classe implementa un metodo come questo, avremo un required
metodo di inizializzazione e incontreremo gli stessi identici problemi che hai incontrato qui con il NSCoding
protocollo.
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 required
metodo 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 init
metodi 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.
Il messaggio di errore che fornisce è un po 'fuorviante:
Argomento aggiuntivo "b" in chiamata
Ma il punto è, Bar
non eredita nessuna delle Foo
's init
metodi, perché non ha soddisfatto nessuno dei due casi particolari per ereditare init
i metodi dalla sua classe padre.
Se questo fosse Objective-C, lo erediteremmo init
senza 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.
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'fatalError
approccio è giusto.