Здраво! Моје име је Кирил Фамин, и ја сам иОС програмер.
Данас желим да детаљно испитам тако једноставну тему као што су иницијализатори у Свифту. Упркос привидној једноставности, понекад недостатак потпуног разумевања ове теме доводи до фрустрирајућих грешака које се жели брзо поправити без упуштања у детаље.
У овом чланку ћемо покрити све у вези са иницијализаторима, укључујући:
Како задржати члански иницијализатор структуре док дефинишете прилагођени
Зашто није увек потребно писати иницијализатор у класама
Зашто позивање super.init
није увек потребно у одређеном иницијализатору
Зашто сва поља поткласе морају бити попуњена пре позивања super.init
Како приступити свим родитељским иницијализаторима са минималним заменама у подкласама
Када је тачно required
иницијализатор
Зашто се UIView.init()
увек позива без параметара, али init(frame:)
и init(coder:)
замењују
...и више. Али идемо корак по корак.
var
вс let
required
иницијализатор: генерици, протоколи, Self()
, final
UIView()
без параметараАпплеов водич Тхе Свифт Программинг Лангуаге (6) (који је, иначе, изненађујуће детаљан за иницијализаторе) каже:
Иницијализација је процес припреме инстанце класе, структуре или набрајања за употребу. Овај процес укључује постављање почетне вредности за свако сачувано својство на тој инстанци и извођење било каквог другог подешавања или иницијализације које је потребно пре него што нова инстанца буде спремна за употребу.
Овај процес иницијализације имплементирате тако што дефинишете иницијализаторе , који су попут посебних метода које се могу позвати да креирају нову инстанцу одређеног типа. За разлику од Објецтиве-Ц иницијализатора, Свифт иницијализатори не враћају вредност. Њихова примарна улога је да осигурају да су нове инстанце типа исправно иницијализоване пре него што се први пут користе.
Па, претпостављам да не морам ништа да додам овде.
Почнимо са дискусијом о иницијализаторима структуре. Ово је прилично једноставно јер нема наслеђа, али ипак постоје нека правила која морате знати.
Хајде да напишемо једноставну структуру:
struct BankAccount { let amount: Double let isBlocked: Bool } let bankAccount = BankAccount(amount: 735, isBlocked: Bool)
Приметите да смо били у могућности да иницијализујемо структуру без експлицитног декларисања иницијализатора. Ово се дешава зато што структуре примају иницијализатор по члану који генерише компајлер. Ово функционише само за структуре .
Ако изаберете Рефактор → Генериши члански иницијализатор , можете видети како то изгледа:
init(amount: Double, isBlocked: Bool) { self.amount = amount self.isBlocked = isBlocked }
Из потписа је лако видети да ће неуспех да обезбедите вредности за све параметре довести до грешке при компилацији:
let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call
Међутим, ако желите да смањите број потребних аргумената, можете дефинисати прилагођени иницијализатор:
init(amount: Double, isBlocked: Bool = false) { self.amount = amount isBlocked = isBlocked } let bankAccount = BankAccount(amount: 735) // ✅
Имајте на уму да ако isBlocked
није попуњен, то ће довести до грешке при компилацији јер сва својства структуре морају бити попуњена у иницијализатору .
var
вс let
Једини случај када поље не треба да се попуни експлицитно је када је то опциона ( ?
) променљива ( var
). У таквим случајевима, поље ће подразумевано бити nil
:
struct BankAccount { let amount: Double var isBlocked: Bool? init(amount: Double) { self.amount = amount } } let bankAccount = BankAccount(amount: 735) // ✅
Међутим, ако покушамо да користимо иницијализатор по чланству у овом случају, добићемо грешку компилације:
let bankAccount = BankAccount( amount: 735, isBlocked: false ) // ❌ Extra argument 'isBlocked' in call
Ово се дешава зато што се декларисањем прилагођеног иницијализатора уклања члански иницијализатор. Још увек је могуће експлицитно дефинисати, али неће бити аутоматски доступан.
Међутим, постоји мали трик да се задржи члански иницијализатор: прогласите прилагођени иницијализатор у 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
Резиме за структуре
var
поља су подразумевана на nil
extension
Примарни иницијализатор за класу је назначени иницијализатор . Служи у две сврхе:
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) } }
Сва поља морају бити попуњена пре позивања 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) } }
Дакле, да нисмо поставили self.breed = breed
, наишли бисмо на грешку у току извођења јер би иницијализатор Animal
позвао преодређени getInfo()
метод из класе Dog
. Овај метод покушава да приступи својству breed
, које још не би било насељено.
За разлику од структура, класе не добијају имплицитни члански иницијализатор. Ако постоје неиницијализована својства, долази до грешке при компилацији:
class Animal { // ❌ Class 'Animal' has no initializers var age: Int }
class Animal { // ✅ var age: Int = 0 }
class Animal { // ✅ var age: Int? }
class Animal { } // ✅
Класе такође могу имати иницијализатор погодности . За разлику од одређених иницијализатора, они не креирају објекат од нуле, већ поједностављују процес иницијализације поновним коришћењем логике других иницијализатора.
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 } }
Иницијализатори погодности могу позвати или одређене иницијализаторе или друге иницијализаторе погодности. На крају, одређени иницијализатор ће увек бити позван.
Погодни иницијализатори увек иду хоризонтално (селф.инит), а назначени иницијализатори иду вертикално (супер.инит).
Чим поткласа прогласи нова својства, она губи приступ свим иницијализаторима погодности суперкласе.
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
Ово се може поправити тако што ће се заобићи сви назначени иницијализатори суперкласе .
class Dog: Animal { // ... override init(age: Int, name: String) { self.breed = "Mixed" super.init(age: age, name: name) } } let dog = Dog(age: 3) // ✅
Лако је видети да, на овај начин, да бисте користили иницијализатор погодности у следећој поткласи, морате заменити два иницијализатора.
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) // ✅
Међутим, ово се може избећи коришћењем иницијализатора за замену погодности .
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) // ✅
Сада имамо само 2 експлицитно специфицирана иницијализатора у свакој подкласи.
Обратите пажњу на то како иницијализатори који заобилазе погодности називају self
уместо super.init
.
Овај трик је детаљно објашњен у 5. поглављу књиге Свифт ин Дептх од Тјеерд ин 'т Веен, књиге коју топло препоручујем.
super.init()
.Већ смо разговарали о томе да ако поткласа не уведе нове параметре, она аутоматски наслеђује све иницијализаторе суперкласе.
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) // ✅
Међутим, постоји још једна важна тачка: ако суперкласа има само један назначен иницијализатор и без параметара ( init()
без аргумената), онда експлицитно декларисани иницијализатори у поткласи не морају да позивају super.init()
. У овом случају, Свифт компајлер аутоматски убацује позив у доступни super.init()
без аргумената.
class Base { init() { } } class Subclass: Base { let secondValue: Int init(secondValue: Int) { self.secondValue = secondValue // ✅ without explicit super.init() } }
Код се компајлира јер се super.init()
имплицитно позива. Ово је кључно за неке од следећих примера.
required
иницијализатор се користи у свим случајевима када поткласа мора имати исти иницијализатор као основна класа. Такође мора позвати super.init()
. Испод су примери где је required
иницијализатор неопходан.
Позивање 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 }
Овај код се не компајлира јер Factory
не зна ништа о подкласама Base
. Иако у овом конкретном случају, Subclass
има init()
без параметара, замислите да је увела ново поље:
class Subclass: Base { let value: Int init(value: Int) { self.value = value } }
Овде више нема празан init
, тако да мора бити декларисан по required
.
class Base { required init() { } } class Subclass: Base { } struct Factory<T: Base> { static func initInstance() -> T { // ✅ T() } } let subclass = Factory<Subclass>.initInstance()
Приметите да иако нисмо експлицитно декларисали required init
у Subclass
, компајлер га је генерисао за нас. О томе се расправљало у Цомпилер Ассистанце . required init
је аутоматски наслеђен и назван super.init()
.
class Subclass: Base { required init() { super.init() } }
Сви иницијализатори декларисани у протоколима морају бити required
:
protocol Initable { init() } class InitableObject: Initable { init() { // ❌ Initializer requirement 'init()' can only // be satisfied by a 'required' initializer } // in non-final class 'InitableObject' }
Опет, ово је неопходно како би компајлер осигурао да поткласа имплементира иницијализатор протокола. Као што већ знамо, то се не дешава увек – ако 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()
Наравно, следећи код се неће компајлирати јер 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 } }
Слична ситуација се дешава када се позива 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 }
Као и увек, проблем је у наслеђивању:
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
Пошто је сврха required
да се наметне имплементација иницијализатора у подкласама, наравно, забрана наслеђивања коришћењем final
кључне речи уклања потребу да се иницијализатор означи као required
.
protocol Initable { init() } final class InitableObject: Initable { } // ✅
protocol ValueInitable { init(value: Int) } final class ValueInitableObject: ValueInitable { init(value: Int) { } // ✅ }
init()
без параметара, она се аутоматски позива у иницијализаторима поткласе.required
је иницијализатор да би се гарантовало његово присуство у подкласама за употребу у генерицима, протоколима и Self()
. Кратак помен иницијализатора UIView()
без параметара, који се не може наћи у документацији, али се мистериозно користи свуда.
Разлог је тај што UIView
наслеђује од NSObject
, који има init()
без параметара. Стога , овај иницијализатор није експлицитно декларисан у 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()
Међутим, испод хаубе, овај иницијализатор позива init(frame:)
када се иницијализује у коду или init(coder:)
када се иницијализује преко Интерфаце Буилдер-а. Ово се дешава зато што UIView
обезбеђује сопствену имплементацију NSObject.init()
, што се може потврдити чињеницом да method_getImplementation
враћа различите адресе за NSObject.init()
и UIView.init()
.
Неуспешан инит је само онај који враћа опциони
final class Failable { let positiveValue: Int init?(value: Int) { guard value > 0 else { return nil } positiveValue = value } }
Енумови са сировом вредношћу добијају бесплатну init?(rawValue:)
enum Direction: String { case north case west case south case east } let north = Direction(rawValue: "north")
Такође можете креирати прилагођени инит за енуме. Све инитс набрајања морају доделити self
.
enum DeviceType { case phone case tablet init(screenWidth: Int) { self = screenWidth > 800 ? .tablet : .phone } }
Покрили смо све битне аспекте иницијализатора у Свифт-у:
У иницијализатору, сва поља морају бити попуњена.
Опциона својства var
су подразумевана на nil
.
Структуре добијају бесплатни иницијализатор по члану .
Члански иницијализатор нестаје када се дефинише прилагођени иницијализатор.
Одређени иницијализатор осигурава да су сва поља попуњена и позива super.init()
.
Погодни иницијализатор поједностављује иницијализацију позивањем одређеног иницијализатора.
Погодни иницијализатори увек иду хоризонтално ( self.init
), а назначени иницијализатори иду вертикално ( super.init
).
Иницијализатор погодности постаје недоступан подкласама ако декларишу нова својства.
Да бисте вратили иницијализатор погодности суперкласе, сви њени назначени иницијализатори морају бити замењени.
Да би се минимизирао број замењивања, може се користити иницијализатор погодности за замена .
Ако поткласа не уводи нове параметре, она аутоматски наслеђује све иницијализаторе из своје суперкласе.
Ако суперкласа има само init()
без параметара, она се аутоматски позива у иницијализаторима поткласе.
Потребан иницијализатор обезбеђује његово присуство у подкласама за употребу у генерицима, протоколима и Self()
.
UIView.init()
позива или UIView.init(frame:)
или UIView.init(coder:)
.
Неуспешан иницијализатор враћа опциони.
Енумови са сировом вредношћу добијају бесплатну init?(rawValue:)
.
Сви иницијализатори енума морају доделити self
.
Надам се да сте пронашли нешто корисно у овом чланку. Ако нешто остане нејасно или нађете нетачност, слободно ме контактирајте за бесплатно објашњење на Телеграму: @кфамин .