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.
var
vs let
required
inicijalizator: generički, protokoli, Self()
, final
UIView()
bez parametaraAppleov 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.
Počnimo s raspravom o inicijalizatorima strukture. Ovo je prilično jednostavno jer nema nasljeđivanja, ali ipak postoje neka pravila koja morate znati.
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:
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 .
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
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
var
polja zadana su na nil
extension
Primarni inicijalizator za klasu je naznačeni inicijalizator . Služi u dvije svrhe:
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 { } // ✅
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 inicijalizatori uvijek idu vodoravno (self.init), a naznačeni inicijalizatori idu okomito (super.init).
Č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
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) // ✅
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) // ✅
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) // ✅
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.
super.init()
.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.
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.
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() } }
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 } }
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) } }
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) { } // ✅ }
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()
. 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()
.
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 } }
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 } }
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 .