paint-brush
Swift init(), una volta per tuttedi@kfamyn
Nuova storia

Swift init(), una volta per tutte

di Kiryl Famin19m2025/03/21
Read on Terminal Reader

Troppo lungo; Leggere

Questo articolo tratta tutti gli aspetti essenziali degli inizializzatori Swift: memberwise, designato, di convenienza, override di convenienza; casi d'uso richiesti; inizializzatore UIView() senza parametri; assistenza del compilatore; init fallibile, init enum e altro ancora.
featured image - Swift init(), una volta per tutte
Kiryl Famin HackerNoon profile picture
0-item
1-item

Introduzione

Ciao! Mi chiamo Kiryl Famin e sono uno sviluppatore iOS.


Oggi voglio esaminare approfonditamente un argomento semplice come gli inizializzatori in Swift. Nonostante la sua apparente semplicità, a volte la mancanza di una comprensione completa di questo argomento porta a errori frustranti che si desidera correggere rapidamente senza addentrarsi nei dettagli.


In questo articolo tratteremo tutto ciò che riguarda gli inizializzatori, tra cui:


  • Come mantenere l'inizializzatore membro per membro della struttura mentre ne si definisce uno personalizzato

  • Perché non è sempre necessario scrivere un inizializzatore nelle classi

  • Perché la chiamata super.init non è sempre richiesta in un inizializzatore designato

  • Perché tutti i campi di una sottoclasse devono essere popolati prima di chiamare super.init

  • Come accedere a tutti gli inizializzatori padre con override minimi nelle sottoclassi

  • Quando è necessario esattamente un inizializzatore required

  • Perché UIView.init() viene sempre chiamato senza parametri, ma init(frame:) e init(coder:) vengono sovrascritti


...e altro ancora. Ma andiamo per gradi.

Sommario

Nozioni di base

Strutture

  • Inizializzatore membro per membro
  • Opzionali
  • var contro let
  • Mantenimento di un inizializzatore membro per membro

Lezioni

  • Inizializzatore designato
  • Inizializzatore di convenienza
  • Mantenimento dell'inizializzatore di convenienza della superclasse
  • Ridurre al minimo il numero di override
  • Assistenza al compilatore
  • inizializzatore required : generici, protocolli, Self() , final
  • UIView() senza parametri

Menzioni d'onore

  • Inizializzazione non riuscita
  • Enumerazioni

Riepilogo

Link rilevanti

Nozioni di base

La guida di Apple The Swift Programming Language (6) (che, tra l'altro, è sorprendentemente dettagliata per gli inizializzatori) afferma:


L'inizializzazione è il processo di preparazione di un'istanza di una classe, struttura o enumerazione per l'uso. Questo processo comporta l'impostazione di un valore iniziale per ogni proprietà archiviata su quell'istanza e l'esecuzione di qualsiasi altra configurazione o inizializzazione richiesta prima che la nuova istanza sia pronta per l'uso.


Si implementa questo processo di inizializzazione definendo gli inizializzatori , che sono come metodi speciali che possono essere chiamati per creare una nuova istanza di un tipo particolare. A differenza degli inizializzatori Objective-C, gli inizializzatori Swift non restituiscono un valore. Il loro ruolo principale è quello di garantire che le nuove istanze di un tipo siano inizializzate correttamente prima di essere utilizzate per la prima volta.


Bene, credo che non ci sia nulla da aggiungere.

Strutture

Cominciamo a discutere degli inizializzatori di struttura. È abbastanza semplice, dato che non c'è ereditarietà, ma ci sono comunque alcune regole che devi conoscere.

Inizializzatore membro per membro

Scriviamo una struttura semplice:

 struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)


Nota che siamo stati in grado di inizializzare la struttura senza dichiarare esplicitamente un inizializzatore. Ciò accade perché le strutture ricevono un inizializzatore memberwise generato dal compilatore. Ciò funziona solo per le strutture .


Selezionando Refactor → Genera inizializzatore membro per membro , puoi vedere come appare:


Generazione di un inizializzatore membro per membro in Xcode


 init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }


Dalla firma è facile vedere che se non si specificano valori per tutti i parametri si verificherà un errore di compilazione:

 let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call


Tuttavia, se si desidera ridurre il numero di argomenti richiesti, è possibile definire un inizializzatore personalizzato:

 init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅


Si noti che se isBlocked non viene popolato, si verificherà un errore di compilazione perché tutte le proprietà della struttura devono essere popolate in un inizializzatore .

Opzionali, var vs let

L'unico caso in cui un campo non deve essere popolato esplicitamente è quando è una variabile facoltativa ( ? ) ( var ). In tali casi, il campo sarà impostato di default su nil :

 struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅


Tuttavia, se proviamo a utilizzare l'inizializzatore memberwise in questo caso, otterremo un errore di compilazione:

 let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call

Mantenimento di un inizializzatore membro per membro

Ciò accade perché dichiarare un inizializzatore personalizzato rimuove l'inizializzatore memberwise. È ancora possibile definirlo esplicitamente, ma non sarà disponibile automaticamente.


Tuttavia, esiste un piccolo trucco per mantenere l'inizializzatore membro per membro: dichiarare l'inizializzatore personalizzato in extension .

 struct BankAccount { let amount: Double var isBlocked: Bool? } extension BankAccount { init(amount: Double) { self.amount = amount } } let barclaysBankAccount = BankAccount(amount: 735) // ✅ let revolutBankAccount = BankAccount(amount: 812, isBlocked: false) // ✅ print(barclaysBankAccount.isBlocked) // nil print(barclaysBankAccount.isBlocked) // false


Riepilogo per le strutture

  • Tutti i campi devono essere popolati in un inizializzatore
  • I campi var facoltativi sono impostati di default su nil
  • Le strutture ricevono un inizializzatore membro gratuito
  • L'inizializzatore membro scompare se viene dichiarato un inizializzatore personalizzato
  • Per mantenere l'inizializzatore membro per membro della struttura, definirne uno personalizzato in extension

Lezioni

Inizializzatore designato

L'inizializzatore primario per una classe è l' inizializzatore designato . Ha due scopi:

  1. Assicura che tutti i campi siano compilati
  2. Se la classe è ereditata, chiama l'inizializzatore della superclasse
 class Animal { var name: String init(name: String) { self.name = name } } class Dog: Animal { var breed: String var name: String init(breed: String, name: String) { self.breed = breed super.init(name: name) } }


Tutti i campi devono essere popolati prima di chiamare super.init . Questo perché l'inizializzatore della superclasse può chiamare metodi sovrascritti dalla sottoclasse, che potrebbero accedere a proprietà della sottoclasse non popolate.

 class Animal { var age: Int init(age: Int) { self.age = age getInfo() } func getInfo() { print("Age: ", age) } } class Dog: Animal { var breed: String init(breed: String, age: Int) { self.breed = breed // imagine we haven't done this super.init(age: age) } override func getInfo() { print("Age: ", age, ", breed: ", breed) } }


Pertanto, se non avessimo impostato self.breed = breed , avremmo riscontrato un errore di runtime perché l'inizializzatore Animal avrebbe chiamato il metodo getInfo() sovrascritto dalla classe Dog . Questo metodo tenta di accedere alla proprietà breed , che non sarebbe ancora popolata.


A differenza delle strutture, le classi non ricevono un inizializzatore implicito per membro. Se ci sono proprietà non inizializzate, si verifica un errore di compilazione:

 class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
 class Animal { // ✅ var age: Int = 0 }
 class Animal { // ✅ var age: Int? }
 class Animal { } // ✅

Inizializzatore di convenienza

Le classi possono anche avere un inizializzatore di convenienza . A differenza degli inizializzatori designati, non creano un oggetto da zero, ma semplificano il processo di inizializzazione riutilizzando la logica di altri inizializzatori.

 class Rectangle { var width: Double var height: Double init(width: Double, height: Double) { self.width = width self.height = height } convenience init(side: Double) { self.init(width: side, height: side) // uses a designated initializer of self } }


Gli inizializzatori di convenienza possono chiamare sia inizializzatori designati che altri inizializzatori di convenienza. In definitiva, verrà sempre chiamato un inizializzatore designato.

Inizializzatori di convenienza, designati e superclassi


Gli inizializzatori di convenienza vanno sempre in orizzontale (self.init), mentre gli inizializzatori designati vanno in verticale (super.init).

Mantenimento dell'inizializzatore di convenienza della superclasse

Non appena una sottoclasse dichiara nuove proprietà, perde l'accesso a tutti gli inizializzatori di convenienza della superclasse.

 class Animal { var age: Int var name: String init(age: Int, name: String) { self.age = age self.name = name } convenience init(age: Int) { self.init(age: age, name: "Default") } convenience init(name: String) { self.init(age: 0, name: name) } } class Dog: Animal { var breed: String init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } let dog = Dog(age: 3) // ❌ Missing arguments for parameters 'breed', 'name' in call 


Gerarchia di inizializzazione corrente, solo un init è dichiarato esplicitamente


Questo problema può essere risolto sovrascrivendo tutti gli inizializzatori designati della superclasse .

 class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅ 


Gli inizializzatori di convenienza sono ora ripristinati, ma due inizializzatori sono dichiarati esplicitamente


È facile vedere che, in questo modo, per utilizzare un inizializzatore di convenienza nella sottoclasse successiva, è necessario sovrascrivere due inizializzatori.

 class GuideDog: Dog { var isTrained: Bool override init(age: Int, name: String) { self.isTrained = false super.init(age: age, name: name) } override init(age: Int, name: String, breed: String) { self.isTrained = false super.init(age: age, name: name, breed: breed) } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅ 


Gli inizializzatori di convenienza sono stati recuperati, ma GuideDog specifica esplicitamente tre inizializzatori


Ridurre al minimo il numero di override

Tuttavia, è possibile evitare questo problema utilizzando un inizializzatore di override di convenienza .

 class Dog: Animal { var breed: String convenience override init(age: Int, name: String) { self.init(age: age, name: name, breed: "Mixed") // self, not super } init(age: Int, name: String, breed: String) { self.breed = breed super.init(age: age, name: name) } } class GuideDog: Dog { var isTrained: Bool // override init(age: Int, name: String) { // self.isTrained = false // // super.init(age: age, name: name, breed: "Mixed") // } convenience override init(age: Int, name: String, breed: String) { self.init(age: age, name: name, breed: breed, isTrained: false) // self, not super } init(age: Int, name: String, breed: String, isTrained: Bool) { self.isTrained = isTrained super.init(age: age, name: name, breed: breed) } } let dog = GuideDog(age: 3) // ✅ 


Sono stati recuperati due inizializzatori di convenienza di Animal, è stata recuperata la convenienza di Dog init(age:, name:) e sono stati specificati esplicitamente solo due inizializzatori di GuideDog


Ora abbiamo solo 2 inizializzatori specificati esplicitamente in ogni sottoclasse.

Si noti come gli inizializzatori di override di convenienza chiamino init self anziché super.init .

Questo trucco è spiegato in dettaglio nel capitolo 5 di Swift in Depth di Tjeerd in 't Veen, un libro che consiglio vivamente.

Riepilogo intermedio

  • Un inizializzatore designato assicura che tutte le proprietà siano popolate e chiama super.init() .
  • Un inizializzatore di convenienza semplifica l'inizializzazione chiamando un inizializzatore designato.
  • Un inizializzatore di convenienza diventa non disponibile per le sottoclassi se dichiarano nuove proprietà.
  • Per ripristinare l'inizializzatore di convenienza di una superclasse, tutti gli inizializzatori da essa designati devono essere sovrascritti.
  • Per ridurre al minimo il numero di override, è possibile utilizzare un inizializzatore di override di convenienza .

Assistenza al compilatore

Abbiamo già spiegato che se una sottoclasse non introduce nuovi parametri, eredita automaticamente tutti gli inizializzatori della superclasse.

 class Base { let value: Int init() { value = 0 } init(value: Int) { self.value = value } } class Subclass: Base { } let subclass = Subclass() // ✅ let subclass = Subclass(value: 3) // ✅


Tuttavia, c'è un altro punto importante: se la superclasse ha un solo inizializzatore designato ed è senza parametri ( init() senza argomenti), allora gli inizializzatori dichiarati esplicitamente nella sottoclasse non hanno bisogno di chiamare super.init() . In questo caso, il compilatore Swift inserisce automaticamente la chiamata al super.init() disponibile senza argomenti.

 class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }


Il codice compila perché super.init() viene chiamato implicitamente. Questo è fondamentale per alcuni degli esempi seguenti.

Necessario

Un inizializzatore required viene utilizzato in tutti i casi in cui una sottoclasse deve avere lo stesso inizializzatore della classe base. Deve anche chiamare super.init() . Di seguito sono riportati esempi in cui è necessario un inizializzatore required .

Generici

È possibile chiamare init su un tipo generico solo dichiarandolo come required init .

 class Base { } class Subclass: Base { } struct Factory<T: Base> { func initInstance() -> T { // ❌ Constructing an object of class T() // type 'T' with a metatype value } // must use a 'required' initializer }


Questo codice non compila perché Factory non sa nulla delle sottoclassi di Base . Sebbene in questo caso particolare, Subclass abbia un init() senza parametri, immagina se introducesse un nuovo campo:

 class Subclass: Base { let value: Int init(value: Int) { self.value = value } }


Qui non c'è più un init vuoto, quindi deve essere dichiarato come required .

 class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()


Nota che anche se non abbiamo dichiarato esplicitamente required init in Subclass , il compilatore lo ha generato per noi. Questo è stato discusso in Compiler Assistance . Il required init è stato automaticamente ereditato e chiamato super.init() .

 class Subclass: Base { required init() { super.init() } }

Protocolli

Tutti gli inizializzatori dichiarati nei protocolli devono essere required :

 protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }


Di nuovo, questo è necessario affinché il compilatore assicuri che la sottoclasse implementi l'inizializzatore di protocollo. Come già sappiamo, questo non sempre accade: se init non è required , la sottoclasse non è obbligata a sovrascriverlo e può definire il proprio inizializzatore.

 class IntValue: InitableObject { let value: Int init(value: Int) { self.value = value } } let InitableType: Initable.Type = IntValue.self let initable: Initable = InitableType.init()


Naturalmente, il codice seguente non verrà compilato perché Base.init() non è required .

 class InitableObject: Initable { required init() { } // ✅ } class IntValue: InitableObject { let value: Int required init() { self.value = 0 } init(value: Int) { self.value = value } }

Se stesso()

Una situazione simile si verifica quando si chiama l'inizializzatore Self() nei metodi statici.

 class Base { let value: Int init(value: Int) { self.value = value } static func instantiate() -> Self { Self(value: 3) // ❌ Constructing an object of class type 'Self' } // with a metatype value must use a 'required' initializer }


Come sempre, il problema risiede nell'ereditarietà:

 class Subclass: BaseClass { let anotherValue: Int init(anotherValue: Int) { self.anotherValue = anotherValue } } let subclass = Subclass.instantiate() // ❌
 class BaseClass { let value: Int required init(value: Int) { // ✅ self.value = value } static func instantiate() -> Self { Self(value: 3) } }

Sbarazzarsi di required : final

Poiché lo scopo di required è quello di imporre l'implementazione di un inizializzatore nelle sottoclassi, naturalmente, vietando l'ereditarietà tramite la parola chiave final si elimina la necessità di contrassegnare un inizializzatore come required .

 protocol Initable { init() } final class InitableObject: Initable { } // ✅
 protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }

Riepilogo intermedio

  • Se una sottoclasse non introduce nuovi parametri, eredita automaticamente tutti gli inizializzatori dalla sua superclasse.
  • Se la superclasse ha solo init() senza parametri, viene chiamata automaticamente negli inizializzatori della sottoclasse.
  • È necessario un inizializzatore required per garantirne la presenza nelle sottoclassi per l'uso in generici, protocolli e Self() .

Visualizzazione dell'interfaccia utente()

Un breve accenno all'inizializzatore UIView() senza parametri, che non si trova nella documentazione ma è misteriosamente utilizzato ovunque.


Il motivo è che UIView eredita da NSObject , che ha un init() senza parametri. Pertanto , questo inizializzatore non è dichiarato esplicitamente nell'interfaccia UIView , ma è comunque disponibile:

 @available(iOS 2.0, *) @MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate { open class var layerClass: AnyClass { get } public init(frame: CGRect) public init?(coder: NSCoder) open var isUserInteractionEnabled: Bool // no init()


Tuttavia, sotto il cofano, questo inizializzatore chiama init(frame:) quando inizializzato nel codice o init(coder:) quando inizializzato tramite Interface Builder. Ciò accade perché UIView fornisce la propria implementazione di NSObject.init() , il che può essere confermato dal fatto che method_getImplementation restituisce indirizzi diversi per NSObject.init() e UIView.init() .

Menzioni d'onore

Inizializzazione non riuscita

Un init fallibile è semplicemente quello che restituisce un valore facoltativo

 final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }

Enumerazione

Gli enum con un valore raw ottengono un init?(rawValue:)

 enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")

Puoi anche creare un init personalizzato per gli enum. Tutti gli init degli enum devono assegnare self .

 enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }

Riepilogo finale

Abbiamo trattato tutti gli aspetti essenziali degli inizializzatori in Swift:


  • In un inizializzatore, tutti i campi devono essere compilati.

  • Le proprietà var facoltative sono impostate di default su nil .

  • Le strutture ricevono un inizializzatore membro per membro .

  • L' inizializzatore membro per membro scompare quando viene definito un inizializzatore personalizzato.

  • Un inizializzatore designato assicura che tutti i campi siano popolati e chiama super.init() .

  • Un inizializzatore di convenienza semplifica l'inizializzazione chiamando un inizializzatore designato.

  • Gli inizializzatori di convenienza vanno sempre in orizzontale ( self.init ), mentre gli inizializzatori designati vanno in verticale ( super.init ).

  • Un inizializzatore di convenienza diventa non disponibile per le sottoclassi se dichiarano nuove proprietà.

  • Per ripristinare l'inizializzatore di convenienza di una superclasse, tutti gli inizializzatori da essa designati devono essere sovrascritti.

  • Per ridurre al minimo il numero di override, è possibile utilizzare un inizializzatore di override di convenienza .

  • Se una sottoclasse non introduce nuovi parametri, eredita automaticamente tutti gli inizializzatori dalla sua superclasse.

  • Se la superclasse ha solo init() senza parametri, viene automaticamente chiamata negli inizializzatori della sottoclasse.

  • Un inizializzatore obbligatorio garantisce la sua presenza nelle sottoclassi per l'uso in generici, protocolli e Self() .

  • UIView.init() chiama UIView.init(frame:) o UIView.init(coder:) .

  • Un inizializzatore non riuscito restituisce un valore facoltativo.

  • Gli enum con un valore raw ottengono un init?(rawValue:) gratuito.

  • Tutti gli inizializzatori di enum devono assegnare self .


Spero che tu abbia trovato qualcosa di utile in questo articolo. Se qualcosa non ti è chiaro o se trovi delle inesattezze, sentiti libero di contattarmi per una spiegazione gratuita su Telegram: @kfamyn .

Link rilevanti

  1. Il linguaggio di programmazione Swift (6) / Inizializzazione
  2. Swift in profondità di Tjeerd in 't Veen
  3. Telegramma - @kfamyn