Il modello di inizializzazione di Swift è stato progettato per eliminare il comportamento non definito comune in linguaggi come Objective-C, dove accedere a metodi o proprietà di istanza prima che tutta la memoria sia inizializzata possa portare a errori di segmentazione o sfruttamenti di sicurezza. Il problema fondamentale risiede nelle gerarchie di classi: un oggetto sottoclasse contiene memoria per le proprie proprietà memorizzate più tutte le proprietà ereditate, e il compilatore deve garantire che nessun codice tocchi questa memoria finché ogni byte non è valido. Per risolvere questo problema, Swift applica un'invariante di inizializzazione definitiva (DI) tramite analisi statica, imponendo che un oggetto rimanga in uno stato parzialmente costruito e non sicuro finché la Fase 1 della sua inizializzazione a due fasi non si conclude. Durante la Fase 1, il costruttore deve assegnare valori a tutte le proprietà introdotte dalla classe corrente e delegare verso i costruttori della superclasse; solo dopo il completamento di questa fase self può essere accessibile o estratto in modo sicuro.
class Vehicle { let wheelCount: Int init(wheels: Int) { self.wheelCount = wheels // Fase 1 completata per Vehicle } } class Bicycle: Vehicle { let hasBell: Bool init(bell: Bool) { // Fase 1: Inizializzare prima le proprie proprietà self.hasBell = bell // Poi delegare alla superclasse super.init(wheels: 2) // Fase 1 completata: inizializzazione definitiva ottenuta // Fase 2: Sicuro utilizzare self self.checkSafety() } func checkSafety() { print("Bici con \(wheelCount) ruote \(hasBell ? "ha" : "non ha") un campanello") } }
Durante lo sviluppo di un'applicazione per cartelle cliniche, ci siamo trovati di fronte a uno scenario complesso con una superclasse PatientRecord e una sottoclasse ICUPatientRecord che richiedeva di calcolare un punteggio di gravità basato sull'età del paziente (una proprietà della superclasse) durante l'inizializzazione. L'implementazione iniziale tentava di chiamare un metodo ausiliario calculateSeverity()—che accedeva a self.age—prima di invocare super.init(age:), risultando in un errore di compilazione perché il costruttore della sottoclasse non aveva ancora garantito la sicurezza della memoria ereditata. Abbiamo valutato tre approcci architettonici distinti per risolvere questo vincolo.
Un approccio prevedeva di dichiarare il punteggio di gravità come un opzione implicitamente opzionale (var severity: Int!) e rimandare la sua assegnazione fino a dopo il completamento dell'inizializzazione della superclasse. Sebbene questo soddisfacesse il compilatore, introdusse un rischio significativo a tempo di esecuzione: la proprietà poteva essere accessibile prima dell'assegnazione, causando un crash, e ci impedì di utilizzare una dichiarazione immutabile let, compromettendo la garanzia di integrità del record.
Una seconda strategia considerava l'uso di un metodo di fabbrica statico che avrebbe istanziato un oggetto segnaposto temporaneo solo per leggere l'età, calcolare la gravità offline e quindi costruire l'istanza reale con valori pre-calcolati. Questo preservava la sicurezza della memoria ma aggiungeva un sostanziale boilerplate e offuscava il flusso di inizializzazione, rendendo il codice significativamente più difficile da mantenere e debug per gli altri membri del team.
La soluzione scelta comportava la ristrutturazione del costruttore per accettare l'età come parametro, calcolando la gravità utilizzando una funzione statica pura che operava sul parametro di input piuttosto che sulla proprietà dell'istanza, e passando il valore pre-calcolato a un costruttore designato. Questo approccio manteneva l'immutabilità consentendo che severity fosse una costante let, rispettava rigorosamente le regole di inizializzazione a due fasi e permetteva al compilatore di verificare la sicurezza al momento della compilazione piuttosto che a tempo di esecuzione. Il risultato fu una sequenza di inizializzazione senza crash che esprimeva chiaramente la dipendenza dei dati tra età e gravità, sfruttando l'analisi statica di Swift per prevenire regressioni.
Perché il compilatore impedisce di chiamare metodi di istanza su self anche se quei metodi sono definiti nella sottoclasse e sembrano non essere correlati alle proprietà della superclasse?
Il compilatore applica questa restrizione perché l'oggetto esiste come memoria allocata, ma la porzione della superclasse rimane memoria grezza non inizializzata. Qualsiasi chiamata a metodo su self—indipendentemente da dove è definita—riceve il puntatore completo all'oggetto e potrebbe potenzialmente accedere ai campi non inizializzati della superclasse attraverso mezzi indiretti, violando la sicurezza della memoria. Swift tratta con cautela tutto l'uso di self prima del completamento della Fase 1 come non sicuro, consentendo solo assegnazioni dirette alle proprietà memorizzate della classe attuale.
Come gestisce l'analisi di inizializzazione definitiva le proprietà di riferimento weak rispetto alle proprietà di riferimento unowned?
Il controllore di inizializzazione definitiva tratta i tipi opzionali, comprese le variabili weak che sono implicitamente Opzionali, come aventi un valore iniziale valido di nil iniettato automaticamente dal compilatore. Di conseguenza, le proprietà weak non richiedono un'inizializzazione esplicita nei costruttori. Al contrario, i riferimenti unowned sono non opzionali e assumono una semantica immediata non nulla; pertanto, devono essere assegnati a un valore prima che il costruttore si completi, proprio come i riferimenti forti, oppure il compilatore genererà un errore di inizializzazione definitiva.
Cosa distingue le regole di delega per i costruttori di convenienza dai costruttori designati riguardo all'inizializzazione definitiva?
I costruttori di convenienza fungono da punti di ingresso secondari che devono delegare a un costruttore designato (tramite self.init) prima di eseguire qualsiasi operazione specifica dell'istanza. Sono severamente vietati dall'inizializzare direttamente le proprietà memorizzate perché il costruttore designato che chiamano porta la responsabilità di soddisfare i requisiti di inizializzazione definitiva. Questo contrasta con i costruttori designati, che devono inizializzare tutte le proprietà introdotte dalla loro classe prima di delegare verso un costruttore della superclasse, garantendo che l'oggetto sia valido a ogni livello della gerarchia.