In questo articolo, esplorerò alcuni dei dettagli grintosi di Swift initializers. Perché affrontare un argomento così eccitante? Principalmente perché occasionalmente mi sono trovato confuso dagli errori di compilazione quando ho provato a creare una sottoclasse di UIView, o quando ho provato a creare una versione Swift di una vecchia classe Objective-C, ad esempio. Così ho deciso di scavare più a fondo per ottenere una migliore comprensione delle sfumature degli inizializzatori Swift. Poiché il mio background / esperienza ha coinvolto principalmente il lavoro con Groovy, Java e Javascript, confronterò alcuni dei meccanismi di inizializzazione di Swift con quelli di Java & Groovy. Vi incoraggio ad accendere un parco giochi in XCode e giocare con gli esempi di codice da soli. Ti incoraggio anche a leggere la sezione di inizializzazione della guida linguistica Swift 2.1, che è la fonte primaria di informazioni per questo articolo. Infine, il mio focus sarà sui tipi di riferimento (classi) piuttosto che sui tipi di valore (strutture).
- Che cos’è un inizializzatore?
- Le sottoclassi generalmente non ereditano gli inizializzatori
- Designated vs Convenience Initializers
- Fasi di inizializzazione
- Sovrascrivendo un inizializzatore
- Quando gli inizializzatori sono ereditati
- Inizializzatori fallibili
- Inizializzatori richiesti
- Caso speciale: Estendere UIView
Che cos’è un inizializzatore?
Un inizializzatore è un tipo speciale di metodo in una struttura, classe o enum Swift che è responsabile di assicurarsi che un’istanza appena creata della struttura, classe o enum sia completamente inizializzata prima che siano pronte per essere utilizzate. Essi svolgono lo stesso ruolo di quello di un “costruttore” in Java e Groovy. Se hai familiarità con Objective-C, dovresti notare che gli inizializzatori Swift differiscono dagli inizializzatori Objective-C in quanto non restituiscono un valore.
Le sottoclassi generalmente non ereditano gli inizializzatori
Una delle prime cose da tenere a mente è che “Le sottoclassi Swift non ereditano i loro inizializzatori superclassi per impostazione predefinita”, secondo la guida linguistica. (La guida spiega che ci sono scenari in cui gli inizializzatori di superclasse vengono ereditati automaticamente. Tratteremo quegli scenari eccezionali più tardi). Questo è coerente con il modo in cui Java (e per estensione, Groovy) funziona. Considera quanto segue:
Proprio come con Java & Groovy, ha senso che questo non è permesso (anche se, come con la maggior parte delle cose, potrebbe esserci qualche argomento su questo punto. Vedi questo post StackOverflow). Se fosse consentito, l’inizializzazione della proprietà “instrument” del musicista verrebbe ignorata, lasciando potenzialmente l’istanza del musicista in uno stato non valido. Tuttavia, con Groovy normalmente non mi preoccuperei di scrivere inizializzatori (cioè costruttori). Piuttosto, userei semplicemente il costruttore di mappe che Groovy fornisce implicitamente, che consente di scegliere liberamente e scegliere quali proprietà si desidera impostare al momento della costruzione. Ad esempio, il seguente è un codice Groovy perfettamente valido:
Si noti che è possibile includere qualsiasi proprietà, incluse quelle fornite dalle superclassi, ma non è necessario specificarle tutte (o nessuna) e non è necessario specificarle in un ordine particolare. Questo tipo di inizializzatore ultra-flessibile non è fornito da Swift. La cosa più vicina in Swift sono gli inizializzatori memberwise forniti automaticamente per le strutture. Ma l’ordine degli argomenti in un inizializzatore memberwise è significativo, anche se sono nominati, e dipende dall’ordine in cui sono definiti:
Comunque, back to classes – La filosofia di Groovy riguardo alla validità degli oggetti post-costruzione è chiaramente molto diversa da quella di Swift. Questo è solo uno dei molti modi in cui Groovy differisce da Swift.
Designated vs Convenience Initializers
Prima di arrivare troppo in basso nella tana del coniglio, dovremmo chiarire un concetto importante: In Swift, un inizializzatore è classificato come un inizializzatore designato o di convenienza. Cercherò di spiegare la differenza sia concettualmente che sintatticamente. Ogni classe deve avere almeno un inizializzatore designato, ma può avere diversi (“designato “non implica”singolo”). Un inizializzatore designato è considerato un inizializzatore primario. Sono gli head-honchos. Sono in ultima analisi responsabili di assicurarsi che tutte le proprietà siano inizializzate. A causa di questa responsabilità, a volte può diventare un dolore usare un inizializzatore designato tutto il tempo, poiché potrebbero richiedere diversi argomenti. Immagina di lavorare con una classe che ha diverse proprietà e devi creare diverse istanze di quella classe che sono quasi identiche ad eccezione di una o due proprietà. (Per motivi di discussione, supponiamo anche che non ci siano valori predefiniti sensibili che potrebbero essere stati assegnati alle proprietà quando vengono dichiarate). Ad esempio, diciamo che la nostra classe Person aveva anche proprietà eatsFood e enjoysMusic. Naturalmente, queste due cose sarebbero impostate su true la maggior parte del tempo, ma non si sa mai. Dai un’occhiata:
Ora la nostra classe Person ha quattro proprietà che devono essere impostate e abbiamo un inizializzatore designato che può fare il lavoro. L’inizializzatore designato è il primo, quello che prende 4 argomenti. La maggior parte delle volte, questi ultimi due argomenti avranno il valore “true”. Sarebbe un dolore dover continuare a specificarli ogni volta che vogliamo creare una persona tipica. Ecco dove entrano gli ultimi due inizializzatori, quelli contrassegnati con il modificatore di convenienza. Questo modello dovrebbe sembrare familiare a uno sviluppatore Java. Se si dispone di un costruttore che richiede più argomenti di quelli di cui si ha realmente bisogno per gestire tutto il tempo, è possibile scrivere costruttori semplificati che prendono un sottoinsieme di tali argomenti e forniscono valori predefiniti ragionevoli per gli altri. Gli inizializzatori di convenienza devono delegare ad un altro, forse meno conveniente, inizializzatore convenienza o ad un inizializzatore designato. In definitiva un inizializzatore designato deve essere coinvolto. Inoltre, se si tratta di una sottoclasse, l’inizializzatore designato deve chiamare un inizializzatore designato dalla sua superclasse immediata.
Un esempio reale per l’uso del modificatore di convenienza proviene dalla classe UIBezierPath di UIKit. Sono sicuro che puoi immaginare che ci siano diversi modi per specificare un percorso. Come tale, UIBezierPath fornisce più convenienza per gli inizializzatori:
pubblico comodità init(rect: CGRect)
pubblico comodità init(ovalInRect rect: CGRect)
pubblico comodità init(roundedRect rect: CGRect, cornerRadius: CGFloat)
pubblico comodità init(roundedRect rect: CGRect, byRoundingCorners angoli: UIRectCorner, cornerRadii: CGSize)
pubblico comodità init(arcCenter centro: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, in senso orario: Bool)
public convenience init(CGPath: CGPath)
In precedenza, ho detto che una classe può avere più inizializzatori designati. Allora, che aspetto ha? Una differenza importante, applicata dal compilatore, tra inizializzatori designati e inizializzatori di convenienza è che gli inizializzatori designati non possono delegare a un altro inizializzatore nella stessa classe (ma devono delegare a un inizializzatore designato nella sua superclasse immediata). Guarda il secondo inizializzatore di Person, quello che prende un singolo argomento chiamato “Non morto”. Poiché questo inizializzatore non è contrassegnato con il modificatore di convenienza, Swift lo considera come un inizializzatore designato. Come tale, non può delegare ad un altro inizializzatore di persona. Prova a commentare le prime quattro righe e a non commentare l’ultima riga. Il compilatore si lamenterà e XCode dovrebbe cercare di aiutarti suggerendo di risolverlo aggiungendo il modificatore di convenienza.
Ora considera la sottoclasse Musicista di Persona. Ha un singolo inizializzatore e deve quindi essere un inizializzatore designato. In quanto tale, deve chiamare un inizializzatore designato della superclasse immediata, Person. Ricorda: mentre gli inizializzatori designati non possono delegare a un altro inizializzatore della stessa classe, gli inizializzatori di convenienza devono farlo. Inoltre, un inizializzatore designato deve chiamare un inizializzatore designato della sua superclasse immediata. Vedere la guida linguistica per maggiori dettagli (e bella grafica).
Fasi di inizializzazione
Come spiega la Swift Language Guide, ci sono due fasi di inizializzazione. Le fasi sono delimitate dalla chiamata all’inizializzatore designato superclasse. La fase 1 è prima della chiamata all’inizializzatore designato superclasse, la fase 2 è dopo. Una sottoclasse deve inizializzare tutte le sue proprietà nella fase 1 e NON può impostare alcuna proprietà definita dalla superclasse fino alla fase 2.
Ecco un esempio di codice, adattato da un esempio fornito nella guida linguistica, che mostra che è necessario inizializzare le proprietà PROPRIE di una sottoclasse prima di richiamare l’inizializzatore designato superclasse. Non è possibile accedere alle proprietà fornite dalla superclasse fino a dopo aver richiamato l’inizializzatore designato superclasse. Infine, non è possibile modificare le proprietà memorizzate costanti dopo che l’inizializzatore designato superclasse è stato richiamato.
Sovrascrivendo un inizializzatore
Ora che siamo convinti che le sottoclassi generalmente non ereditano gli inizializzatori, e siamo chiari sul significato e sulla distinzione tra inizializzatori designati e convenienza, consideriamo cosa succede quando si desidera che una sottoclasse sovrascriva un inizializzatore dalla sua superclasse immediata. Ci sono quattro scenari che mi piacerebbe coprire, dato che ci sono due tipi di inizializzatore. Quindi prendiamoli uno per uno, con semplici esempi di codice per ogni caso:
Un inizializzatore designato che corrisponde a un inizializzatore designato dalla superclasse
Questo è uno scenario tipico. Quando si esegue questa operazione, è necessario applicare il modificatore override. Si noti che questo scenario è in vigore anche quando si sta “sovrascrivendo” un inizializzatore predefinito fornito automaticamente (cioè quando la superclasse non definisce alcun inizializzatore esplicito. In questo caso, Swift ne fornisce uno implicitamente. Gli sviluppatori Java dovrebbero avere familiarità con questo comportamento). Questo inizializzatore predefinito fornito automaticamente è sempre un inizializzatore designato.
Un inizializzatore designato che corrisponde a un inizializzatore di convenienza superclasse
Ora supponiamo di voler aggiungere un inizializzatore designato alla sottoclasse che corrisponde a un inizializzatore di convenienza nel genitore. Secondo le regole della delega di inizializzatore stabilite nella guida linguistica, l’inizializzatore designato dalla sottoclasse deve delegare fino a un inizializzatore designato della superclasse immediata. Cioè, non puoi delegare fino all’inizializzatore di convenienza corrispondente del genitore. Per motivi di discussione, supponiamo anche che la sottoclasse non si qualifichi per ereditare gli inizializzatori della superclasse. Quindi, dal momento che non è mai possibile creare un’istanza della sottoclasse invocando direttamente l’inizializzatore di convenienza superclasse, tale inizializzatore di convenienza corrispondente non è, e non potrebbe mai essere, coinvolto nel processo di inizializzazione in ogni caso. Pertanto, non lo stai sovrascrivendo e il modificatore di override non si applica.
Un inizializzatore di convenienza che corrisponde a un inizializzatore designato dalla superclasse
In questo scenario, immagina di avere una sottoclasse che aggiunge le proprie proprietà il cui valore predefinito può essere (ma non deve essere) calcolato dai valori assegnati a una o più proprietà della classe genitore. Supponiamo che tu voglia anche solo avere un inizializzatore designato per la tua sottoclasse. È possibile aggiungere un inizializzatore di convenienza alla sottoclasse la cui firma corrisponde a quella di un inizializzatore designato della classe genitore. In questo caso, il nuovo inizializzatore avrebbe bisogno sia della comodità che dei modificatori di override. Ecco un esempio di codice valido per illustrare questo caso:
Un inizializzatore di convenienza che corrisponde a un inizializzatore di convenienza superclasse
Se si desidera aggiungere un inizializzatore di convenienza alla sottoclasse che corrisponde alla firma di un inizializzatore di convenienza della superclasse, basta andare avanti. Come ho spiegato sopra, non puoi comunque sovrascrivere gli inizializzatori di convenienza. Quindi includi il modificatore di convenienza, ma ometti il modificatore di override e trattalo come qualsiasi altro inizializzatore di convenienza.
Una chiave da asporto da questa sezione è che il modificatore override viene utilizzato solo, e deve essere utilizzato, se si sta sovrascrivendo un inizializzatore designato superclasse. (Piccolo chiarimento da fare qui: se stai sovrascrivendo un inizializzatore richiesto, allora useresti il modificatore richiesto invece del modificatore di override. Il modificatore richiesto implica il modificatore di override. Vedere la sezione Inizializzatori richiesti di seguito).
Quando gli inizializzatori sono ereditati
Ora per gli scenari di cui sopra in cui vengono ereditati gli inizializzatori di superclasse. Come spiega la Guida linguistica Swift, se la sottoclasse fornisce valori predefiniti per tutte le sue proprietà alla dichiarazione e non definisce nessuno dei propri inizializzatori designati, erediterà automaticamente tutti i suoi inizializzatori designati dalla superclasse. Oppure, se la sottoclasse fornisce un’implementazione di tutti gli inizializzatori designati dalla superclasse, eredita automaticamente tutti gli inizializzatori di convenienza della superclasse. Ciò è coerente con la regola in Swift che l’inizializzazione delle classi (e delle strutture) non deve lasciare le proprietà memorizzate in uno stato indeterminato.
Mi sono imbattuto in un comportamento “interessante” mentre sperimentavo inizializzatori di convenienza, inizializzatori designati e regole di ereditarietà. Ho scoperto che è possibile impostare un circolo vizioso inavvertitamente. Si consideri il seguente esempio:
La classe RecipeIngredient sovrascrive tutti gli inizializzatori designati per la classe Food e quindi eredita automaticamente tutti gli inizializzatori di convenienza della superclasse. Ma l’inizializzatore di convenienza alimentare delega ragionevolmente al proprio inizializzatore designato, che è stato sovrascritto dalla sottoclasse RecipeIngredient. Quindi non è la versione Food di quell’inizializzatore init(name: String) che viene invocato, ma la versione sovrascritta in RecipeIngredient. La versione sovrascritta sfrutta il fatto che la sottoclasse ha ereditato l’inizializzatore di convenienza del cibo, ed eccolo lì – hai un ciclo. Non lo so se questo sarebbe considerato un errore del programmatore o un bug del compilatore (l’ho segnalato come bug: https://bugs.swift.org/browse/SR-512 ). Immagina che il cibo sia una classe di una terza parte e non hai accesso al codice sorgente in modo da non sapere come è effettivamente implementato. Non saprai (fino al runtime) che usarlo nel modo mostrato in questo esempio ti intrappolerebbe in un ciclo. Quindi penso che sarebbe meglio se il compilatore ci aiutasse qui.
Inizializzatori fallibili
Immagina di aver progettato una classe con determinati invarianti e di voler applicare tali invarianti dal momento in cui viene creata un’istanza della classe. Ad esempio, forse stai modellando una fattura e vuoi assicurarti che l’importo sia sempre non negativo. Se hai aggiunto un inizializzatore che accetta un argomento di importo di tipo Double, come puoi assicurarti di non violare il tuo invariante? Una strategia è semplicemente verificare se l’argomento non è negativo. Se lo è, usalo. In caso contrario, di default a 0. Ad esempio:
Questo funzionerebbe e potrebbe essere accettabile se si documenta ciò che sta facendo il proprio inizializzatore (specialmente se si prevede di rendere la classe disponibile ad altri sviluppatori). Ma si potrebbe avere un momento difficile difendere quella strategia, dal momento che lo fa tipo di spazzare il problema sotto il tappeto. Un altro approccio supportato da Swift sarebbe quello di far fallire l’inizializzazione. Cioè, renderesti il tuo inizializzatore fallibile.
Come descritto in questo articolo, inizializzatori failable sono stati aggiunti a Swift come un modo per eliminare o ridurre la necessità di metodi di fabbrica, “che in precedenza erano l’unico modo per segnalare errori” durante la costruzione di oggetti. Per rendere un inizializzatore fallibile, è sufficiente aggiungere il ? oppure ! carattere dopo la parola chiave init (cioè, init? o init! ). Quindi, dopo che tutte le proprietà sono state impostate e tutte le altre regole relative alla delega sono state soddisfatte, si aggiungerebbe una logica per verificare che gli argomenti siano validi. Se non sono validi, si attiva un errore di inizializzazione con return nil. Si noti che questo non implica che l’inizializzatore restituisca mai nulla. Ecco come potrebbe apparire la nostra classe di fattura con un inizializzatore fallibile:
Si noti qualcosa di diverso su come stiamo usando il risultato della creazione dell’oggetto? E ‘ come se lo stessimo trattando come un optional, giusto? Beh, è esattamente quello che stiamo facendo! Quando usiamo un inizializzatore failable, otterremo nil (se è stato attivato un errore di inizializzazione) o un Optional(Fattura). Cioè, se l’inizializzazione ha avuto successo, finiremo con un Optional che avvolge l’istanza della fattura a cui siamo interessati, quindi dobbiamo scartarla. (Per inciso, si noti che Java ha anche Optionals a partire da Java 8).
Gli inizializzatori failable sono proprio come gli altri tipi di inizializzatori che abbiamo discusso rispetto all’override e alla delega, designated vs convenience, ecc. In effetti, puoi persino sovrascrivere un inizializzatore failable con un inizializzatore non disponibile. Tuttavia, non è possibile sovrascrivere un inizializzatore non disponibile con uno fallibile.
Potresti aver notato inizializzatori failable dal trattare con UIView o UIViewController, che forniscono entrambi un init di inizializzazione failable?(codificatore aDecoder: NSCoder). Questo inizializzatore viene chiamato quando la vista o ViewController viene caricato da un pennino. È importante capire come funzionano gli inizializzatori fallibili. Ti consiglio vivamente di leggere la sezione Inizializzatori fallibili della guida linguistica Swift per una spiegazione approfondita.
Inizializzatori richiesti
Il modificatore richiesto viene utilizzato per indicare che tutte le sottoclassi devono implementare l’inizializzatore interessato. A prima vista, sembra piuttosto semplice e diretto. A volte può diventare un po ‘ confuso se non si capisce come entrano in gioco le regole relative all’ereditarietà degli inizializzatori discusse sopra. Se una sottoclasse soddisfa i criteri in base ai quali vengono ereditati gli inizializzatori di superclasse, il set di inizializzatori ereditati include quelli contrassegnati come obbligatori. Pertanto, la sottoclasse soddisfa implicitamente il contratto imposto dal modificatore richiesto. Implementa gli inizializzatori richiesti, semplicemente non lo vedi nel codice sorgente.
Se una sottoclasse fornisce un’implementazione esplicita (cioè non ereditata) di un inizializzatore richiesto, sovrascrive anche l’implementazione della superclasse. Il modificatore richiesto implica l’override, quindi il modificatore di override non viene utilizzato. Puoi includerlo se vuoi, ma farlo sarebbe ridondante e XCode ti infastidirà.
La guida linguistica Swift non dice molto sul modificatore richiesto, quindi ho preparato un esempio di codice (vedi sotto) con commenti per spiegare il suo scopo e descrivere come funziona. Per ulteriori informazioni, vedere questo articolo di Anthony Levings.
Caso speciale: Estendere UIView
Una delle cose che mi ha spinto a scavare in profondità negli inizializzatori Swift è stato il mio tentativo di trovare un modo per creare un set di inizializzatori designati senza duplicare la logica di inizializzazione. Ad esempio, stavo lavorando a questo tutorial UIView di Ray Wenderlich, convertendo il suo codice Objective-C in Swift mentre andavo (puoi dare un’occhiata alla mia versione Swift qui). Se guardi quel tutorial, vedrai che la sua sottoclasse RateView di UIView ha un metodo baseInit che entrambi gli inizializzatori designati usano per eseguire attività di inizializzazione comuni. Questo mi sembra un buon approccio: non vuoi duplicare quella roba in ciascuno di quegli inizializzatori. Volevo ricreare quella tecnica nella mia versione Swift di RateView. Ma ho trovato difficile perché un inizializzatore designato non può delegare a un altro inizializzatore nella stessa classe e non può chiamare i metodi della propria classe fino a quando non delega all’inizializzatore superclasse. A quel punto, è troppo tardi per impostare proprietà costanti. Ovviamente, potresti aggirare questa limitazione non usando le costanti, ma questa non è una buona soluzione. Quindi ho pensato che fosse meglio fornire solo valori predefiniti per le proprietà memorizzate in cui sono dichiarate. Questa è ancora la soluzione migliore che attualmente conosco. Tuttavia, ho trovato una tecnica alternativa che utilizza gli inizializzatori.
Dai un’occhiata al seguente esempio. Questo è un frammento di RateViewWithConvenienceInits.swift, che è una versione alternativa della mia porta Swift di RateView. Essendo una sottoclasse di UIView che non fornisce valori predefiniti per tutte le sue proprietà memorizzate alla dichiarazione, questa versione alternativa di RateView deve almeno fornire un’implementazione esplicita dell’init richiesto di UIView?(codificatore aDecoder: NSCoder) inizializzatore. Vorremo anche fornire un’implementazione esplicita dell’init(frame di UIView: CGRect) inizializzatore per assicurarsi che il processo di inizializzazione sia coerente. Vogliamo che le nostre proprietà memorizzate siano impostate allo stesso modo indipendentemente dall’inizializzatore utilizzato.
Si noti che ho aggiunto il modificatore di convenienza alle versioni sovrascritte degli inizializzatori di UIView. Ho anche aggiunto un inizializzatore designato fallibile alla sottoclasse, a cui entrambi gli inizializzatori sovrascritti (convenienza) delegano. Questo singolo inizializzatore designato si occupa di impostare tutte le proprietà memorizzate (comprese le costanti – non ho dovuto ricorrere all’utilizzo di var per tutto). Funziona, ma penso che sia piuttosto kludgy. Preferirei fornire solo valori predefiniti per le mie proprietà memorizzate dove sono dichiarate, ma è bene sapere che questa opzione esiste se necessario.