paint-brush
Swift init(), jednom zauvijekpo@kfamyn
Nova povijest

Swift init(), jednom zauvijek

po Kiryl Famin19m2025/03/21
Read on Terminal Reader

Predugo; Čitati

Ovaj članak pokriva sve što je bitno za Swift inicijalizatore: po članu, naznačeno, pogodnost, nadjačavanje pogodnosti; potrebni slučajevi upotrebe; UIView() inicijalizator bez parametara; pomoć pri sastavljanju; failable init, enum init i više.
featured image - Swift init(), jednom zauvijek
Kiryl Famin HackerNoon profile picture
0-item
1-item

Uvod

Zdravo! Moje ime je Kiryl Famin i ja sam iOS programer.


Danas želim temeljito proučiti tako jednostavnu temu kao što su inicijalizatori u Swiftu. Unatoč prividnoj jednostavnosti, ponekad nedostatak potpunog razumijevanja ove teme dovodi do frustrirajućih pogrešaka koje se želi brzo popraviti bez zalaženja u detalje.


U ovom ćemo članku pokriti sve što se odnosi na inicijalizatore, uključujući:


  • Kako zadržati inicijalizator strukture po članu dok definirate prilagođeni

  • Zašto nije uvijek potrebno pisati inicijalizator u klasama

  • Zašto pozivanje super.init nije uvijek potrebno u određenom inicijalizatoru

  • Zašto sva polja podklase moraju biti popunjena prije pozivanja super.init

  • Kako pristupiti svim nadređenim inicijalizatorima uz minimalna nadjačavanja u podklasama

  • Kada je točno potreban required inicijalizator

  • Zašto se UIView.init() uvijek poziva bez parametara, ali init(frame:) i init(coder:) nadjačani


...i više. Ali idemo korak po korak.

Sadržaj

Osnove

Strukture

  • Inicijalizator po članu
  • Mogućnosti
  • var vs let
  • Zadržavanje inicijalizatora po članu

Nastava

  • Određeni inicijalizator
  • Praktičan inicijalizator
  • Zadržavanje prikladnog inicijalizatora superklase
  • Smanjivanje broja nadjačavanja
  • Pomoć kompilatora
  • required inicijalizator: generički, protokoli, Self() , final
  • UIView() bez parametara

Časna priznanja

  • Neuspješno pokretanje
  • Nabrajanja

Sažetak

Relevantne poveznice

Osnove

Appleov vodič The Swift Programming Language (6) (koji je, usput rečeno, iznenađujuće detaljan za inicijalizatore) navodi:


Inicijalizacija je proces pripreme instance klase, strukture ili enumeracije za upotrebu. Ovaj proces uključuje postavljanje početne vrijednosti za svako pohranjeno svojstvo na toj instanci i izvođenje bilo kojeg drugog postavljanja ili inicijalizacije koji su potrebni prije nego što nova instanca bude spremna za upotrebu.


Ovaj proces inicijalizacije implementirate definiranjem inicijalizatora , koji su poput posebnih metoda koje se mogu pozvati za stvaranje nove instance određenog tipa. Za razliku od Objective-C inicijalizatora, Swift inicijalizatori ne vraćaju vrijednost. Njihova primarna uloga je osigurati da su nove instance tipa ispravno inicijalizirane prije nego što se koriste po prvi put.


Pa, pretpostavljam da ne moram ništa dodati ovdje.

Strukture

Počnimo s raspravom o inicijalizatorima strukture. Ovo je prilično jednostavno jer nema nasljeđivanja, ali ipak postoje neka pravila koja morate znati.

Inicijalizator po članu

Napišimo jednostavnu strukturu:

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


Primijetite da smo mogli inicijalizirati strukturu bez eksplicitnog deklariranja inicijalizatora. To se događa zato što strukture primaju inicijalizator po članu koji generira kompilator. Ovo radi samo za strukture .


Odabirom Refactor → Generate memberwise initializer možete vidjeti kako to izgleda:


Generiranje inicijalizatora po članu u Xcodeu


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


Iz potpisa je lako vidjeti da će nenavođenje vrijednosti za sve parametre rezultirati pogreškom kompilacije:

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


Međutim, ako želite smanjiti broj potrebnih argumenata, možete definirati prilagođeni inicijalizator:

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


Imajte na umu da ako isBlocked nije popunjen, to će rezultirati pogreškom kompilacije jer sva svojstva strukture moraju biti popunjena u inicijalizatoru .

Optionals, var vs let

Jedini slučaj u kojem se polje ne mora eksplicitno popuniti je kada je opcijska ( ? ) varijabla ( var ). U takvim slučajevima, polje će biti zadano postavljeno na nil :

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


Međutim, ako pokušamo upotrijebiti inicijalizator po članu u ovom slučaju, dobit ćemo pogrešku kompilacije:

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

Zadržavanje inicijalizatora po članu

To se događa jer se deklariranjem prilagođenog inicijalizatora uklanja inicijalizator po članu. I dalje ga je moguće eksplicitno definirati, ali neće biti automatski dostupan.


Međutim, postoji mali trik za zadržavanje inicijalizatora po članu: deklarirajte prilagođeni inicijalizator u 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


Sažetak za strukture

  • Sva polja moraju biti popunjena u inicijalizatoru
  • Neobavezna var polja zadana su na nil
  • Strukture dobivaju besplatni inicijalizator po članu
  • Inicijalizator po članu nestaje ako se deklarira prilagođeni inicijalizator
  • Da biste zadržali inicijalizator strukture po članu, definirajte prilagođeni u extension

Nastava

Određeni inicijalizator

Primarni inicijalizator za klasu je naznačeni inicijalizator . Služi u dvije svrhe:

  1. Osigurava da su sva polja popunjena
  2. Ako je klasa naslijeđena, ona poziva inicijalizator superklase
 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) } }


Sva polja moraju biti popunjena prije pozivanja super.init . To je zato što inicijalizator superklase može pozvati metode nadjačane od strane podklase, koje bi mogle pristupiti nepopunjenim svojstvima podklase.

 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) } }


Dakle, da nismo postavili self.breed = breed , naišli bismo na pogrešku vremena izvođenja jer bi inicijalizator Animal pozvao nadjačanu metodu getInfo() iz klase Dog . Ova metoda pokušava pristupiti svojstvu breed , koje još nije popunjeno.


Za razliku od struktura, klase ne primaju implicitni inicijalizator po članu. Ako postoje neinicijalizirana svojstva, javlja se pogreška kompilacije:

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

Praktičan inicijalizator

Klase također mogu imati praktični inicijalizator . Za razliku od određenih inicijalizatora, oni ne stvaraju objekt od nule, već pojednostavljuju proces inicijalizacije ponovnim korištenjem logike drugih inicijalizatora.

 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 } }


Pogodni inicijalizatori mogu pozivati ili naznačene inicijalizatore ili druge prikladne inicijalizatore. U konačnici, uvijek će se pozivati određeni inicijalizator.

Pogodni, naznačeni inicijalizatori superklase


Pogodni inicijalizatori uvijek idu vodoravno (self.init), a naznačeni inicijalizatori idu okomito (super.init).

Zadržavanje praktičnog inicijalizatora superklase

Čim podklasa deklarira nova svojstva, gubi pristup svim pomoćnim inicijalizatorima superklase.

 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 


Trenutna hijerarhija inicijalizatora, samo jedan init je deklariran eksplicitno


To se može popraviti nadjačavanjem svih naznačenih inicijalizatora superklase .

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


Pogodni inicijalizatori sada su ponovno dobiveni, ali su dva inicijalizatora eksplicitno deklarirana


Lako je vidjeti da, na ovaj način, za korištenje pomoćnog inicijalizatora u sljedećoj podklasi, morate nadjačati dva inicijalizatora.

 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) // ✅ 


Pogodni inicijalizatori su vraćeni, ali GuideDog eksplicitno navodi tri inicijalizatora


Smanjivanje broja nadjačavanja

Međutim, to se može izbjeći upotrebom inicijalizatora za praktično nadjačavanje .

 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) // ✅ 


Vraćaju se dva prikladna inicijalizatora za životinje, ponovno se stječe init(age:, ime:) za pse i eksplicitno su navedena samo dva inicijalizatora za psa GuideDog


Sada imamo samo 2 eksplicitno navedena inicijalizatora u svakoj podklasi.

Primijetite kako inicijalizatori nadjačavanja pogodnosti nazivaju self init umjesto super.init .

Ovaj trik je temeljito objašnjen u 5. poglavlju knjige Swift in Depth autora Tjeerda in 't Veena, knjige koju toplo preporučujem.

Međusažetak

  • Određeni inicijalizator osigurava da su sva svojstva popunjena i poziva super.init() .
  • Pogodni inicijalizator pojednostavljuje inicijalizaciju pozivanjem određenog inicijalizatora.
  • Pogodni inicijalizator postaje nedostupan podklasama ako deklariraju nova svojstva.
  • Za vraćanje prikladnog inicijalizatora superklase, svi njegovi naznačeni inicijalizatori moraju biti nadjačani.
  • Kako bi se broj nadjačavanja sveo na najmanju moguću mjeru, može se koristiti praktični inicijalizator nadjačavanja .

Pomoć kompilatora

Već smo raspravljali da ako potklasa ne uvodi nove parametre, ona automatski nasljeđuje sve inicijalizatore superklase.

 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) // ✅


Međutim, postoji još jedna važna točka: ako superklasa ima samo jedan naznačeni inicijalizator i on je bez parametara ( init() bez argumenata), tada eksplicitno deklarirani inicijalizatori u podklasi ne moraju pozivati super.init() . U ovom slučaju, prevodilac Swift automatski umeće poziv na dostupni super.init() bez argumenata.

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


Kod se kompilira jer se super.init() implicitno poziva. Ovo je ključno za neke od sljedećih primjera.

Potreban

required inicijalizator se koristi u svim slučajevima kada podklasa mora imati isti inicijalizator kao osnovna klasa. Također mora pozvati super.init() . Ispod su primjeri u kojima je required inicijalizator.

Generici

Pozivanje init na generičkom tipu moguće je samo deklariranjem kao 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 }


Ovaj kod se ne kompajlira jer Factory ne zna ništa o podklasama Base . Iako u ovom konkretnom slučaju, Subclass ima init() bez parametara, zamislite da uvodi novo polje:

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


Ovdje više nema prazno init , pa se mora deklarirati kao required .

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


Primijetite da iako nismo eksplicitno deklarirali required init u Subclass , prevodilac ga je generirao za nas. O tome se raspravljalo u pomoći za kompajler . required init je automatski naslijeđen i pozvan super.init() .

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

Protokoli

Svi inicijalizatori deklarirani u protokolima moraju biti required :

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


Opet, ovo je neophodno kako bi prevodilac osigurao da podklasa implementira inicijalizator protokola. Kao što već znamo, to se ne događa uvijek—ako init nije required , podklasa ga nije obavezna nadjačati i može definirati vlastiti inicijalizator.

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


Naravno, sljedeći kod se neće kompilirati jer Base.init() nije required .

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

Self()

Slična situacija se događa kada se poziva inicijalizator Self() u statičkim metodama.

 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 }


Kao i uvijek, problem je u nasljeđu:

 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) } }

Oslobađanje od required : final

Budući da je svrha required nametnuti implementaciju inicijalizatora u potklasama, naravno, zabrana nasljeđivanja pomoću ključne riječi final uklanja potrebu za označavanjem inicijalizatora kao required .

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

Međusažetak

  • Ako podklasa ne uvodi nove parametre, ona automatski nasljeđuje sve inicijalizatore iz svoje nadklase.
  • Ako nadklasa ima samo init() bez parametara, automatski se poziva u inicijalizatorima podklase.
  • required je inicijalizator kako bi se zajamčila njegova prisutnost u potklasama za korištenje u generičkim oblicima, protokolima i Self() .

UIView()

Kratko spominjanje UIView() inicijalizatora bez parametara, koji se ne može pronaći u dokumentaciji, ali se misteriozno koristi posvuda.


Razlog je što UIView nasljeđuje od NSObject , koji ima init() bez parametara. Stoga ovaj inicijalizator nije eksplicitno deklariran u UIView sučelju, ali je još uvijek dostupan:

 @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()


Međutim, ispod haube, ovaj inicijalizator poziva init(frame:) kada se inicijalizira u kodu ili init(coder:) kada se inicijalizira putem Interface Buildera. To se događa jer UIView pruža vlastitu implementaciju NSObject.init() , što se može potvrditi činjenicom da method_getImplementation vraća različite adrese za NSObject.init() i UIView.init() .

Časna priznanja

Neuspješno pokretanje

Init koji nije uspio je samo onaj koji vraća opciju

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

Enum

Enumi sa sirovom vrijednošću dobivaju besplatno init?(rawValue:)

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

Također možete stvoriti prilagođeni init za enume. Svi enum initi moraju dodijeliti self .

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

Konačni sažetak

Pokrili smo sve bitne aspekte inicijalizatora u Swiftu:


  • U inicijalizatoru sva polja moraju biti popunjena.

  • Izborna svojstva var zadana su na nil .

  • Strukture dobivaju besplatni inicijalizator po članu .

  • Inicijalizator po članu nestaje kada se definira prilagođeni inicijalizator.

  • Određeni inicijalizator osigurava da su sva polja popunjena i poziva super.init() .

  • Pogodni inicijalizator pojednostavljuje inicijalizaciju pozivanjem određenog inicijalizatora.

  • Pogodni inicijalizatori uvijek idu vodoravno ( self.init ), a naznačeni inicijalizatori idu okomito ( super.init ).

  • Pogodni inicijalizator postaje nedostupan podklasama ako deklariraju nova svojstva.

  • Za vraćanje prikladnog inicijalizatora superklase, svi njegovi naznačeni inicijalizatori moraju biti nadjačani.

  • Kako bi se broj nadjačavanja sveo na najmanju moguću mjeru, može se koristiti praktični inicijalizator nadjačavanja .

  • Ako podklasa ne uvodi nove parametre, ona automatski nasljeđuje sve inicijalizatore iz svoje nadklase.

  • Ako nadklasa ima samo init() bez parametara, automatski se poziva u inicijalizatorima podklase.

  • Potreban inicijalizator osigurava svoju prisutnost u podklasama za korištenje u generičkim oblicima, protokolima i Self() .

  • UIView.init() poziva UIView.init(frame:) ili UIView.init(coder:) .

  • Neuspjeli inicijalizator vraća opciju.

  • Enumi sa sirovom vrijednošću dobivaju besplatno init?(rawValue:) .

  • Svi inicijalizatori enuma moraju dodijeliti self .


Nadam se da ste u ovom članku pronašli nešto korisno. Ako nešto ostane nejasno ili pronađete netočnost, slobodno me kontaktirajte za besplatno objašnjenje na Telegramu: @kfamyn .

Relevantne poveznice

  1. Programski jezik Swift (6) / Inicijalizacija
  2. Swift in Depth autora Tjeerda in 't Veena
  3. Telegram - @kfamyn