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 di questo argomento porta a errori frustranti che si desidera correggere rapidamente senza addentrarsi nei dettagli. completa 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 non è sempre richiesta in un inizializzatore designato super.init 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é viene sempre chiamato senza parametri, ma e vengono sovrascritti UIView.init() init(frame:) init(coder:) ...e altro ancora. Ma andiamo per gradi. Sommario Nozioni di base Strutture Inizializzatore membro per membro Opzionali contro var 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 : generici, protocolli, , required Self() final senza parametri UIView() Menzioni d'onore Inizializzazione non riuscita Enumerazioni Riepilogo Link rilevanti Nozioni di base La guida di Apple (che, tra l'altro, è sorprendentemente dettagliata per gli inizializzatori) afferma: The Swift Programming Language (6) è 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. L'inizializzazione Si implementa questo processo di inizializzazione definendo , 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. gli inizializzatori 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 generato dal compilatore. Ciò funziona . inizializzatore memberwise solo per le strutture Selezionando , puoi vedere come appare: Refactor → Genera inizializzatore membro per membro 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 non viene popolato, si verificherà un errore di compilazione perché . isBlocked tutte le proprietà della struttura devono essere popolate in un inizializzatore vs Opzionali, var let L'unico caso in cui un campo non deve essere popolato è quando è una ( ) ( ). In tali casi, il campo sarà impostato di default su : esplicitamente variabile facoltativa ? var 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' . Ha due scopi: inizializzatore designato Assicura che tutti i campi siano compilati 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 di chiamare . Questo perché l'inizializzatore della superclasse può chiamare metodi sovrascritti dalla sottoclasse, che potrebbero accedere a proprietà della sottoclasse non popolate. devono essere popolati prima super.init 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 , avremmo riscontrato un errore di runtime perché l'inizializzatore avrebbe chiamato il metodo sovrascritto dalla classe . Questo metodo tenta di accedere alla proprietà , che non sarebbe ancora popolata. self.breed = breed Animal getInfo() Dog breed 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 . A differenza degli inizializzatori designati, non creano un oggetto da zero, ma semplificano il processo di inizializzazione riutilizzando la logica di altri inizializzatori. inizializzatore di convenienza 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. 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 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) // ✅ È facile vedere che, in questo modo, per utilizzare un inizializzatore di convenienza nella sottoclasse successiva, è necessario sovrascrivere inizializzatori. due 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) // ✅ 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) // ✅ Ora abbiamo solo 2 inizializzatori specificati esplicitamente in ogni sottoclasse. init anziché . Si noti come gli inizializzatori di override di convenienza chiamino self super.init Questo trucco è spiegato in dettaglio nel capitolo 5 di di Tjeerd in 't Veen, un libro che consiglio vivamente. Swift in Depth Riepilogo intermedio Un assicura che tutte le proprietà siano popolate e chiama . inizializzatore designato super.init() Un semplifica l'inizializzazione chiamando un inizializzatore designato. inizializzatore di convenienza Un diventa non disponibile per le sottoclassi se dichiarano nuove proprietà. inizializzatore di convenienza Per ripristinare di una superclasse, tutti gli inizializzatori da essa designati devono essere sovrascritti. l'inizializzatore di convenienza 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 ( senza argomenti), allora gli inizializzatori dichiarati esplicitamente nella sottoclasse di chiamare . In questo caso, il compilatore Swift la chiamata al disponibile senza argomenti. init() non hanno bisogno super.init() inserisce automaticamente super.init() class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } } Il codice compila perché viene chiamato implicitamente. Questo è fondamentale per alcuni degli esempi seguenti. super.init() Necessario Un inizializzatore viene utilizzato in tutti i casi in cui una sottoclasse deve avere lo stesso inizializzatore della classe base. Deve anche chiamare . Di seguito sono riportati esempi in cui è necessario un inizializzatore . required super.init() required Generici È possibile chiamare su un tipo generico solo dichiarandolo come . init 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é non sa nulla delle sottoclassi di . Sebbene in questo caso particolare, abbia un senza parametri, immagina se introducesse un nuovo campo: Factory Base Subclass init() class Subclass: Base { let value: Int init(value: Int) { self.value = value } } Qui non c'è più un vuoto, quindi deve essere dichiarato come . init 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 in , il compilatore lo ha generato per noi. Questo è stato discusso in . Il è stato automaticamente ereditato e chiamato . required init Subclass Compiler Assistance required init 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 non è , la sottoclasse non è obbligata a sovrascriverlo e può definire il proprio inizializzatore. init required 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é non è . Base.init() 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 nei metodi statici. Self() 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 è quello di imporre l'implementazione di un inizializzatore nelle sottoclassi, naturalmente, vietando l'ereditarietà tramite la parola chiave si elimina la necessità di contrassegnare un inizializzatore come . required final 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 senza parametri, viene chiamata automaticamente negli inizializzatori della sottoclasse. init() È necessario un inizializzatore per garantirne la presenza nelle sottoclassi per l'uso in generici, protocolli e . required Self() Visualizzazione dell'interfaccia utente() Un breve accenno all'inizializzatore senza parametri, che non si trova nella documentazione ma è misteriosamente utilizzato ovunque. UIView() Il motivo è che eredita da , che ha un senza parametri. , questo inizializzatore non è dichiarato esplicitamente nell'interfaccia , ma è comunque disponibile: UIView NSObject init() Pertanto UIView @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 quando inizializzato nel codice o quando inizializzato tramite Interface Builder. Ciò accade perché fornisce la propria implementazione di , il che può essere confermato dal fatto che restituisce indirizzi diversi per e . init(frame:) init(coder:) UIView NSObject.init() method_getImplementation NSObject.init() 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à facoltative sono impostate di default su . var nil Le strutture ricevono un . inizializzatore membro per membro L' quando viene definito un inizializzatore personalizzato. inizializzatore membro per membro scompare Un assicura che tutti i campi siano popolati e chiama . inizializzatore designato super.init() Un semplifica l'inizializzazione chiamando un inizializzatore designato. inizializzatore di convenienza Gli inizializzatori di convenienza vanno sempre ( ), mentre gli inizializzatori designati vanno ( ). in orizzontale self.init in verticale super.init Un diventa non disponibile per le sottoclassi se dichiarano nuove proprietà. inizializzatore di convenienza Per ripristinare di una superclasse, tutti gli inizializzatori da essa designati devono essere sovrascritti. l'inizializzatore di convenienza 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 senza parametri, viene automaticamente chiamata negli inizializzatori della sottoclasse. init() Un garantisce la sua presenza nelle sottoclassi per l'uso in generici, protocolli e . inizializzatore obbligatorio Self() chiama o . UIView.init() UIView.init(frame:) UIView.init(coder:) Un restituisce un valore facoltativo. inizializzatore non riuscito Gli enum con un valore raw ottengono un gratuito. init?(rawValue:) 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 Il linguaggio di programmazione Swift (6) / Inizializzazione Swift in profondità di Tjeerd in 't Veen Telegramma - @kfamyn