Подразделы:
- Customizing Initialization — кастомизированная инициализация
- Default Initializers – инициализаторы по умолчанию
- Memberwise Initializers for Structure Types (Почленный инициализатор для структуры)
- Initializer Delegation for Value Types — Делегирование инициализаторов для значимых типов
- Class Inheritance and Initialization (Наследование классов и инициализация)
- Two-Phase Initialization – двухфазовая инициализация
- Failable Initializers (Проваливающиеся инициализаторы)
- Required Initializers – требуемые инициализаторы
- Setting a Default Property Value with a Closure or Function (Выставление значения по умолчанию для хранимого свойства с помощью замыкания или функции)
Ключевое слово init
Инициализация — это процесс подготовки экземпляра класса, структуры или перечисления. Основная задача — проставление начальных значений для свойств. В отличии от Objective-C, методы инициализации ничего не возвращают.
Вспоминая главу Optionals (опциональные типы или опционалы) — экземплярам типа не может быть присвоено nil, экземпляр ОБЯЗАН иметь значение. Задача инициализатора проверить что все хранимые свойства к концу инициализации получат свои начальные значения (свойства могут быть nil только если они optional типа).
Кстати значения хранимым свойствам выставленные в процессе инициализации — не вызывают willSet/didSet даже если есть наблюдатели
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Shop { let shopName = "Best Shop" //1 var goodsCount : Int //2 var customerIds : [Int]? //3 var lastIncome: Int! //4 var allSum = 0 init() { goodsCount = 0 //5 } func someMethod() { allSum += lastIncome } } var shop = Shop() var ids = [100, 212, 343] shop.customerIds = ids // shop.someMethod() // 6 print(shop.allSum) // выведет 0 shop.lastIncome = 15 shop.someMethod() print(shop.allSum) // выведет 15 |
1. свойству проставлено значение по умолчанию
2. у свойства нет значения по умолчанию, так что мы обязаны выставить его при инициализации (смотри сноску 5)
3. свойства имеющее optional тип, что инициализирует его пресловутым nil по умолчанию
4. Implicitly Unwrapped Optional — мы берем на себя ответственность, что на момент использования свойство будет иметь значение, но это снимает необходимость делать это в инициализаторе
6. если раскомментировать этот код — программа упадет с ошибкой, т.к. на этот момент goodsCount значения не имеет
Customizing Initialization — кастомизированная инициализация
Мы можем кастомизировать инициализацию с помощью переданных параметров. Т.к. init — по сути метод но ограниченный этим именем, все локальные имена параметров автоматически становятся внешними, тобишь обязательными. И все так же можно избежать необходимости использовать внешние имена с помощью символа подчеркивания _
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct Color { let red, green, blue: Double init(red: Double, green: Double, blue: Double) { self.red = red self.green = green self.blue = blue } init(_ white: Double) { red = white green = white blue = white } } let magenta = Color(red: 1.0, green: 0.0, blue: 1.0) let halfGray = Color(0.5) |
В наследниках переназначать константы нельзя кстати
Default Initializers — инициализаторы по умолчанию
Для структур и базовых классов у которых все свойства имеют значение по умолчанию доступны инициализаторы по умолчанию.
1 2 3 4 5 6 |
class ShoppingListItem { var name: String? var quantity = 1 var purchased = false } var item = ShoppingListItem() |
Такая возможность пропадет если создать init метод
Memberwise Initializers for Structure Types (Почленный инициализатор для структуры)
Если структура не имеет своего определенного метода инициализации — она автоматически получает почленный инициализатор. Причем параметры нужно передавать в том же порядке в котором объявлены свойства
1 2 3 4 |
struct Size { var width = 0.0, height = 0.0 } let twoByTwo = Size(width: 1.0, height: 2.0) |
Это работает только если не создан нормальный init
Initializer Delegation for Value Types — Делегирование инициализаторов для значимых типов
У структур и перечислений все просто, т.к. не поддерживается наследование, поэтому единственный нюанс в возможности вызвать из параметризированного init другой init через self.init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct Figure { var width = 0.0, height = 0.0 init(width : Double, height :Double) { self.width = width self.height = height } init(side: Double) { self.init(width: side, height: side) } } let rectangle = Figure(width: 1.0, height: 2.0) let square = Figure(side: 3.0) |
Class Inheritance and Initialization (Наследование классов и инициализация)
С классами все несколько сложнее в плане инициализации, т.к. есть возможность наследования.
Существует 2 вида инициализаторов: designited (основной) и Convenience (вспомогательный). Суть в том, что designited — обычно один, от силы несколько, он базовый.
1 2 3 4 |
init () { } |
А convenience — вспомогательные, из них обычно вызываются основные
1 2 3 4 |
convenience init () { self.init() //вызов designited } |
Существует 3 правила при инициализации
1) designited инициализатор в классе наследника обязан вызвать designited инициализатор его прямого суперкласса (нельзя вызвать инициализатор «деда», а не «отца»)
2) convenience инициализатор может вызывать другие инициализаторы только из этого же класса
3) convenience инициализатор обязан В ИТОГЕ вызывать designited инициализатор, т.е. convenience может вызвать convenience, и сколько угодно раз так может продолжаться, но в итоге должен произойти вызов именно designited инициализатора
Two-Phase Initialization — двухфазовая инициализация
Инициализация классов делится на 2 фазы:
1) Класс должен выставить начальные значения всем хранимым свойствам которые именно он создает.
2) Каждый класс получает возможность переназначить значения хранимых свойств в направлении от базового класса к наследникам
Компилятор swift проводит 4 проверки безопасности для того, чтобы убедиться что правила не нарушены
Проверка 1.
Designited инициализатор обязан выставить значение для всех свойств введенных в этом классе до вызова super.init
Проверка 2
В Designited инициализаторах нельзя присваивать значения свойствам базового класса до вызова super.init
Проверка 3
Convenience инициализатор не имеет права обращаться к любым свойствам до вызова другого инициализатора.
Проверка 4
Convenience инициализатор не имеет права вызывать метод до вызова другого инициализатора.
Вот пример где показаны типовые ошибки
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class Base { var baseX = 10 init() {} } class Figure : Base { var width, height : Double init(width : Double, height :Double) { self.width = width self.height = height // 1 //baseX = 15 // 2 super.init() baseX = 15 // кастомизация } convenience init(xz: Int) { //width = 10.0 // 3 self.init(side:3.0) width = 10.0 // кастомизация } convenience init(side: Double) { //self.test() // 4 self.init(width: side, height: side) self.test() // все ок } func test() { } } let rectangle = Figure(width: 1.0, height: 2.0) let square = Figure(side: 3.0) |
1) если бы этого присваивания не было — вылетела ошибка в процессе проверки 1
2) это выявляется во время 2й проверки, попытка присвоить свойство базового классы до вызова super.init
3) выявляет 3я проверка, обращение к свойству до вызова другого инициализатора
4) выявляет 4я проверка, вызов метода до вызова другого инициализатора
По сути процесс инициализации сначала выполняем наши 4 проверки у инициализируемого класса, затем у его родителя и так пока не дойдем до класса у которого нет родителя.
После этого начинается кастомизация и мы возвращаемся к инициализируемому классу — по ходу прохождения выполняем кастомизацию
В общем случае инициализаторы не наследуются автоматически, если у потомка планируется инициализатор с той же сигнатурой — мы обязаны его переопределить, если переопределяемым инициализатором будет выступать designited — необходимо использовать ключевое слово override, если convenience — этого делать не нужно
Но, есть частные случаи, когда потомок все-таки наследует все или часть инициализаторов
1) Если потомок не создает новых designited инициализаторов (что означает, что он не вводит новых свойств без значения по умолчанию ) — он наследует все designited инициализаторы своего родителя
2) Если потомок предоставляет реализацию для ВСЕХ designited инициализаторов родителя, либо не вводит их вообще (как в пункте 1) — он наследует все convenience инициализаторы своего родителя
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
class Parent { var price: Double var name: String init(name: String, price: Double) { self.name = name self.price = price } init(price: Double, name: String) { self.name = name self.price = price } convenience init() { self.init(name: "Noname", price: 0.0) } } class SimpleChild : Parent { } class OverrideChild : Parent { var count: Int convenience override init(name: String, price: Double) { self.init(name: name, price: price, count: 0) } convenience override init(price: Double, name: String) { self.init(name: name, price: price, count: 0) } init(name: String, price: Double, count: Int) { self.count = 0 super.init(name: name, price: price) } } var simpleChild = SimpleChild() // 1 var child = OverrideChild() // 2 |
1) Этот вызов возможен, т.к. мы не определили новых designited инициализаторов, следовательно выполнили пункт 1 и получили доступ ко всем designited инициализаторам родителя, что привело к автоматическому выполнению условий необходимых для пункта 2, что позволило нам получить доступ к convenience инициализаторам родителя
2) Этот вызов возможен, т.к. мы переопределил ВСЕ designited инициализаторы родителя (пусть и сделали их convenience), т.е. выполнили условия 2го пункта, что позволило нам получить доступ к convenience инициализаторам родителя
Failable Initializers (Проваливающиеся инициализаторы)
Инициализаторы с возможностью выдать ошибку и не завершить процесс инициализации
Решается это с помощью пометки инициализатора вопросительным знаком (init?), чтобы провалить инициализацию — необходимо внутри init? сделать return nil
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
enum Person { case Man, Woman init?(symbol: Character) { switch symbol { case "M": self = .Man case "W": self = .Woman default: return nil } } } if let man = Person(symbol: "M") { print ("good") // выведет good } if let it = Person(symbol: "B") { print ("good") } else { print("bad") // выведет bad } |
Если перечисление использует сырое значение — оно автоматически получает инициализатор init?(rawValue:)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
enum Person : Character { case Man = "M", Woman = "W" } if let man = Person(rawValue: "M") { print ("good") // выведет good } if let it = Person(rawValue: "B") { print ("good") } else { print("bad") // выведет bad } |
В документации и для Swift 1.2 и для Swift 2.0 указано, что если значимый тип может вернуть nil в любом месте инициализатора, то в классе такое позволено только после того как все свойства этого класса получат свои значения
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Song { let name: String let length: Double init(name: String, length: Double) { self.name = name self.length = length } convenience init?(name: String) { //if name.isEmpty { return nil } // 1 self.init(name: name, length:0.0) // 2 if name.isEmpty { return nil } } } var song = Song(name: "") // nil |
1 — компилятор Swift 1.2 выдаст ошибку, т.к. на этот момент свойства name и length еще не получили своих значений.
Но в Swift 2.0 убрали требование на выставление всех свойств до выброса nil но только для convenience инициализаторов
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Song { let name: String let length: Double init?(name: String, length: Double) { return nil // ошибка } convenience init?(name: String) { return nil // нет ошибки } } |
2 — как видим из init? мы можем вызывать обычные инициализаторы, как из этого же класса, так и из родительского
init? можно переопределять в потомках, при переопределении можно сделать failable инициализатор — обычным. Из обычного init нельзя вызывать failable
Так же можно определить init!, правила использование те же, но после получения обьекта не нужно его будет приводить к конкретному типу
Required Initializers — требуемые инициализаторы
Если в родительском классе инициализатор обозначен как требуемый с помощью ключевого слова required — во всех детях он должен быть так же быть проимплементирован: по умолчанию (если не назначено новых designited инициализаторов), и напрямую (с указанием required), если designited инициализатор был добавлен
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class SomeClass { var test = 0 required init() { // initializer implementation goes here } } class SomeSubclass: SomeClass { // required init() { // // subclass implementation of the required initializer goes here // } init(str: String) { } } var someSubClass = SomeSubclass() |
Этот код будет выдавать ошибку, т.к. мы добавили designited инициализатор, и не переопределили required init. Убрать ошибку можно 2мя способами.
Или закомментировать init(str: String), таким образом required init будет использоваться родительский, или раскоментировать required init, в таком случае мы явно выполним требование в переопределении инициализатора
Setting a Default Property Value with a Closure or Function (Выставление значения по умолчанию для хранимого свойства с помощью замыкания или функции)
Если значение по умолчанию формируется сложно в прямом виде, и логичней его вычислить с помощью алгоритма — добавлена возможность использовать замыкание или функцию для этого.
в общем виде синтаксис следующий:
1 2 3 4 5 6 7 |
class SomeClass { let someProperty: SomeType = { var someValue: SomeType // присваиваем желаемое значение someValue return someValue }() } |
Вот реальный пример, где массив arr инициализируется при создании класса 15ю степенями двойки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class ComplexProperty { let arr: [Int] = { var arr = [Int]() arr.append(1) for i in 1...15 { arr.append(arr[i-1] * 2) } return arr }() // 1 } var complex = ComplexProperty() for i in complex.arr { print(i) // выведет 15 степений двойки } |
1. обращаю внимание на наличие скобок, это заставит замыкание выполниться сразу, иначе будет попытка присвоить переменной само замыкание, а не результат его выполнения