Link Search Menu Expand Document

Ініціалізація

Ініціалізація – це процес підготовки екземпляру класу, структури чи перечислення до використання. Цей процес включає присвоєння початкових значень усім властивостям цього екземпляру, що зберігаються, та виконання будь-якого іншого налаштування, що є обов’язковим перед використанням екземпляру.

Процес ініціалізації реалізовується через визначення ініціалізаторів, котрі є чимось на кшталт спеціальних методів, що можуть викликатись для створення нового екземпляру певного типу. На відміну від ініціалізаторів Objective-C, ініціалізатори у Swift не повертають значення. Їх головною роллю є забезпечення того, щоб нові екземпляри типу були коректно проініціалізовані перед першим використанням.

Екземпляри класів також можуть реалізовувати деініціалізатори, що виконують будь-яке спеціальне очищення перед тим, як екземпляр класу буде деалоковано. Детальніше з деініціалізаторами можна ознайомитись в розділі Deinitialization.

Присвоєння початкових значень властивостям, що зберігаються

Класи та структури зобов’язані присвоїти відповідне початкове значення усім властивостям, що зберігаються, до моменту створення екземпляру цього класу чи структури. Властивості, що зберігаються, не можуть залишитись у невизначеному стані.

Початкове значення властивості, що зберігається, можна присвоїти в ініціалізаторі, або шляхом присвоєння значення за замовчанням в момент оголошення властивості. Ці дії описані в наступних підрозділах.

Примітка

Коли ви присвоюєте значення за замовчанням властивості, що зберігається, або задаєте її початкове значення в ініціалізаторі, значення властивості присвоюється напряму, без виклику будь-яких спостерігачів за властивістю.

Ініціалізатори

Ініціалізатори викликаються для створення нового екземпляру нового типу. У найпростішій формі, ініціалізатор схожий на метод екземпляру без параметрів, записаний за допомогою ключового слова init:

init() {
    // ініціалізація буде проходити тут
}

У прикладі нижче визначено нову структуру на ім’я Fahrenheit для зберігання температур, що виражаються у шкалі Фаренгейта. Структура Fahrenheit має одну властивість, що зберігається, на ім’я temperature, котра має тип Double:

struct Fahrenheit {
    var temperature: Double
    init() {
        temperature = 32.0
    }
}
var f = Fahrenheit()
print("Темрература за замовчанням дорівнює \(f.temperature)°F")
// Надрукує "Темрература за замовчанням дорівнює 32.0°F"

Дана структура визначає єдиний ініціалізатор, init, без жодних параметрів, котрий ініціалізує властивість, що зберігається, temperature, значенням 32.0 (точка замерзання води у градусах Фаренгейта).

Значення властивостей за замовчанням

Початкові значення властивостей, що зберігаються, можна присвоювати в ініціалізаторі, як показано вище. Іншим способом є задавання значення за замовчанням властивості в ході її оголошення. Для цього значення за замовчанням присвоюється властивості в момент її оголошення.

Примітка

Якщо властивість завжди має одне й те ж саме початкове значення, бажано задавати це значення у вигляді значення за замовчанням, а не присвоювати його в ініціалізаторі. Результат буде однаковий, однак значення за замовчанням прив’язує ініціалізацію властивості близько до її оголошення. Це дозволяє мати більш чисті, коротші ініціалізатори, і дозволяє компілятору визначати тип властивості за її значенням за замовчанням. Значення за замовчанням також дозволяє скористатись перевагами ініціалізаторів за замовчанням, та наслідуванням ініціалізаторів, як показано далі у цьому розділі.

Можемо переписати структуру Fahrenheit з прикладу вище у простішій формі, надавши значення за замовчанням її властивості temperature в момент оголошення цієї властивості:

struct Fahrenheit {
    var temperature = 32.0
}

Параметризація ініціалізації

Можна налаштувати ініціалізацію під свої потреби за допомогою вхідних параметрів, надаючи властивостям опціональні типи, або шляхом присвоєння значень константним властивостям під час ініціалізації, як показано в наступних підрозділах.

Параметри ініціалізації

Ініціалізатори можуть мати параметри ініціалізації, котрі визначають типи та назви значень, що дозволяють налаштувати процес ініціалізації. Параметри ініціалізації мають такі ж можливості та синтаксис, що і параметри функцій та методів.

У наступному прикладі оголошено структуру на ім’я Celsius, що зберігає температуру, що виражена у градусах Цельсія. Структура Celsius реалізує два власних ініціалізатори init(fromFahrenheit:) та init(fromKelvin:), котрі ініціалізують нові екземпляри структури значенням з іншої температурної шкали:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)    // точка кипіння води
// boilingPointOfWater.temperatureInCelsius дорівнює 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)      // точка замерзання води
// freezingPointOfWater.temperatureInCelsius дорівнює 0.0

Перший ініціалізатор має єдиний параметр ініціації з міткою аргументу fromFahrenheit та іменем параметра fahrenheit. Другий ініціалізатор має єдиний параметр ініціації з міткою аргументу fromKelvin та іменем параметра kelvin. Обидва ініціалізатори конвертують їх єдиний аргумент у відповідне значення у шкалі Цельсія, і зберігають це значення у властивості на ім’я temperatureInCelsius.

Імена параметрів на мітки аргументів

Як і параметри функції та методів, параметри ініціалізації можуть мати ім’я параметра (для використання всередині тіла ініціалізатори) та мітку аргументу (для використання при виклику ініціалізатора).

Однак, ініціалізатори не мають спеціального імені функції перед їх круглими дужками, на відміну від функцій та методів. Внаслідок цього, імена та типи параметрів ініціалізації грають особливо важливу роль в ідентифікації того, який з ініціалізаторів слід викликати. Через це, Swift надає мітку аргументу для кожного параметра ініціалізації автоматично, якщо її не було задано вами.

У наступному прикладі оголошено структуру на ім’я Color, із трьома константними властивостями red, green, та blue. Кожна з цих властивостей зберігає значення в діапазоні між 0.0 та 1.0 для індикації кількості червоної, зеленої та синьої компоненти в кольорі.

Структура Color має ініціалізатор з трьома відповідними іменами параметрів типу Double для її червоної, зеленої та синьої компоненти. Структура Color також має другий ініціалізатор з єдиним параметром white, що використовується для задання однакового значення всім трьом компонентам.

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

Обидва ініціалізатори можуть використовуватись для створення нового екземпляру структури Color, надаючи іменовані значення для всіх параметрів ініціалізації:

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

Слід помітити, що неможливо викликати ці ініціалізатори без використання міток аргументів. Мітки аргументів завжди повинні використовуватись в ініціалізаторі, їх пропуск призводить до помилки часу компіляції:

let veryGreen = Color(0.0, 1.0, 0.0)
// це призведе до помилки часу компіляції – мітки аргументів обов'язкові

Параметри ініціалізації без мітки аргументу

Якщо ви не хочете використовувати мітки аргументу в параметрі ініціалізації, можна написати символ підкреслення (_) замість явної мітки аргументу для параметра, щоб вимкнути поведінку за замовчанням.

Ось розширена версія структури Celsius з прикладу вище, із додатковим ініціалізатором для створення нового екземпляру структури Celsius зі значення типу Double, що вже виражене у шкалі Цельсія:

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius дорівнює 37.0

Виклик ініціалізатора Celsius(37.0) має чіткий намір не потребує мітки аргументу. Тому доречно записувати цей ініціалізатор як init(_ celsius: Double), щоб було можливо викликати його не іменуючи значення типу Double.

Опціональні властивості

У деяких ситуаціях буває логічним дозволити властивості, що зберігається, не мати жодного значення. Наприклад, коли це значення не є відомим під час ініціалізації, або коли допускається не мати значення пізніше. В таких випадках слід оголошувати властивість з опціональним типом. Властивості опціонального типу автоматично ініціалізуються значенням nil, що позначає свідомий намір не мати значення під час ініціалізації.

У наступному прикладі оголошено клас на ім’я SurveyQuestion, котрий моделює питання з опитування, та має опціональну властивість типу String на ім’я response:

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let cheeseQuestion = SurveyQuestion(text: "Вам подобається бринза?")
cheeseQuestion.ask()
// Надрукує "Вам подобається бринза?"
cheeseQuestion.response = "Так, мені подобається бринза."

Відповідь на питання з опитування є невідомою до того, як це питання було задане, тому властивість response має тип String?, або “опціональний String”. Під час ініціалізації нового екземпляру класу SurveyQuestion властивості response було автоматично присвоєно значення за замовчанням nil, що означає “поки що нема рядка”.

Присвоєння значень константним властивостям під час ініціалізації

Константній властивості можна присвоїти значення в будь-який момент ініціалізації, за умови що її значення буде визначеним до кінця ініціалізації. Як тільки константній властивості було присвоєно значення, його більше не можна змінити.

Примітка

У випадку екземплярів класу, константну властивість можна змінити лише у тому класі, де її оголошено. Її не можна змінити у класі-нащадку.

Можна переглянути клас SurveyQuestion у прикладі вище, зробивши властивість text константною замість змінної. Це дозволяє позначити, що питання не змінюється після створення екземпляру класу SurveyQuestion. Хоч властивість text і є константною, їй можна присвоїти значення в ініціалізаторі класу:

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "Як щодо буряка?")
beetsQuestion.ask()
// Надрукує "Як щодо буряка?"
beetsQuestion.response = "Я й буряк люблю теж. (Але не з бринзою.)"

Ініціалізатори за замовчанням

Swift автоматично створює ініціалізатори за замовчанням для будь-якої структури або класу, кожна властивість яких має значення за замовчанням, і в яких нема хоча б одного ініціалізатора. Ініціалізатор за замовчанням просто створює новий екземпляр, усі властивості якого ініціалізовано їх значеннями за замовчанням.

У наступному прикладі оголошено клас на ім’я ShoppingListItem, котрий інкапсулює назву, кількість та стан покупки елементу зі списку покупок:

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}
var item = ShoppingListItem()

Оскільки усі властивості класу ShoppingListItem мають значення за замовчанням, і оскільки клас ShoppingListItem є базовим класом без батьківського класу, він автоматично отримує ініціалізатор за замовчанням, що створює новий екземпляр, та присвоює значення за замовчанням кожній властивості. (Властивість name має опціональний тип String, і тому вона автоматично отримує значення nil, хоч це значення і не записано в коді). У прикладі вище для створення нового екземпляру класу ShoppingListItem використовується ініціалізатор за замовчанням, котрий викликається за допомогою звичайного синтаксису ініціалізації ShoppingListItem(), після чого цей новий екземпляр присвоєно змінній item.

Почленні ініціалізатори для структур

Структури автоматично отримують почленні ініціалізатори, якщо в них не оголошено жодного власного ініціалізатора. На відміну від ініціалізатора за замовчанням, почленний ініціалізатор структури буде створено навіть якщо в ній є властивості, що зберігаються та не мають значення за замовчанням.

Почленний ініціалізатор є скороченим способом проініціалізувати властивості нового екземпляру структури. Початкові значення властивостей нового екземпляру структури передаються до почленного ініціалізатора за іменем.

У прикладі нижче оголошено структуру на ім’я Size, із двома властивостями width та height. Тип обох властивостей визначено як Double через присвоєння значення за замовчанням 0.0.

Структура Size автоматично отримує почленний ініціалізатор init(width:height:), котрий можна використовувати для створення нових екземплярів структури Size:

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

Делегування ініціалізації у типах-значеннях

Ініціалізатори можуть викликати інші ініціалізатори для виконання частини роботи з ініціалізації екземпляру. Цей процес називають делегуванням ініціалізації. Він дозволяє уникнути дуплікації коду поміж декількох ініціалізаторів.

Правила роботи делегування ініціалізації, та дозволені види делегування ініціалізації, різняться для типів-значень та класів. Типи-значення (структури та перечислення) не підтримують наслідування, і тому процес делегування їх ініціалізації є порівняно простим: одні їх ініціалізатори просто викликають інші ініціалізатори з цього ж типу. Класи ж можуть наслідуватись від інших класів, як описано в розділі Наслідування. Це означає, що ініціалізатори класів мають додаткові зобов’язання: слід впевнитись, що всі успадковані властивості, що зберігаються, отримають відповідне значення після ініціалізації. Ці обов’язки описані в підрозділі Наслідування та ініціалізація класів нижче.

У ініціалізаторах типів-значень можна посилатись на інші ініціалізатори цього ж типу-значення за допомогою self.init. self.init можна викликати лише в іншому ініціалізаторі.

Слід зазначити, якщо було явно визначено ініціалізатор типу-значення, втрачається доступ до ініціалізатора за замовчанням (або почленного ініціалізатора, якщо це структура) цього типу. Це обмеження дозволяє уникнути ситуації, в якій можна обійти суттєві додаткові налаштування з явного ініціалізатора, випадково викликавши один з автоматичних ініціалізаторів.

Примітка

Якщо потрібно, щоб тип-значення мав як ініціалізатор за замовчанням та почленний ініціалізатор, так і явно заданий спеціальний ініціалізатор, слід оголошувати явно задані ініціалізатори в розширеннях, а не в тілі реалізації самого типу. Детальніше з розширеннями можна ознайомитись в розділі Розширення.

В наступному прикладі визначено структуру Rect, що представляє прямокутник. Rect описується шляхом використання допоміжних структур, Size та Point, що представляють розмір та початкову точку відповідно. Усі властивості обох структур мають значення за замовчанням 0.0:

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

Структуру Rect можна ініціалізувати одним із трьох способів:

  • використовуючи значення за замовчанням властивостей origin та size, тобто нулі
  • надаючи значення початкової точки (origin) та розміру
  • надаючи значення центру та розміру

Всі ці способи представлені трьома явними ініціалізаторами всередині оголошення структури Rect:

struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

Перший ініціалізатор, init(), є функціонально ідентичним ініціалізатору за замовчанням, котрий структура отримала б у випадку, якби в неї не було інших явних ініціалізаторів. Цей ініціалізатор має порожнє тіло, представлене порожньою парою фігурних дужок {}. Виклик цього ініціалізатора поверне екземпляр структури Rect, що має початкову точку та розмір проініціалізовані значеннями за замовчанням, тобто Point(x: 0.0, y: 0.0) and Size(width: 0.0, height: 0.0):

let basicRect = Rect()
// початковою точкою basicRect є (0.0, 0.0), а розміром - (0.0, 0.0)

Другий ініціалізатор, init(origin:size:), є функціонально ідентичним почленному ініціалізатору, котрий структура отримала б, якби в неї не було інших явних ініціалізаторів. Цей ініціалізатор просто присвоює значення своїх аргументів властивостям origin та size:

let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// початковою точкою originRect є (2.0, 2.0), а розміром - (5.0, 5.0)

Третій ініціалізатор, init(center:size:), є трохи складнішим. Він починається з обчислення початкової точки за центром та розміром прямокутника. Далі йде виклик (або делегування) ініціалізатора init(origin:size:), котрий зберігає обчислену початкову точку та розмір у відповідних властивостях:

let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))
// початковою точкою centerRect's є (2.5, 2.5), а розміром - (3.0, 3.0)

Ініціалізатор init(center:size:) міг би присвоювати нові значення властивостям origin та size самостійно. Однак, більш зручно (та зрозуміліше за наміром) користуватись перевагами існуючого ініціалізатора, в якому вже реалізовано дану функціональність.

Примітка

Альтернативний спосіб переписати приклад вище без явного визначення ініціалізаторів init() and init(origin:size:) можна віднайти в розділі Розширення.

Наслідування та ініціалізація класів

У класах, всім властивостям, що зберігаються, включно із властивостями, що клас наслідує з батьківського класу, повинні бути присвоєні початкові значення під час ініціалізації.

У Swift є два види ініціалізаторів для типів-класів, що допомагає забезпечити присвоєння початкового значення усім властивостям, що зберігаються. Це призначені ініціалізатори та ініціалізатори для зручності.

Призначені ініціалізатори та ініціалізатори для зручності

Призначений ініціалізатор є основним ініціалізатором класу. Призначений ініціалізатор повністю ініціалізує всі властивості, що оголошено в даному класі, та викликає відповідний ініціалізатор батьківського класу, щоб продовжити процес ініціалізації по ланцюжку батьківських класів.

Класи мають тенденцію мати не дуже багато призначених ініціалізаторів, і є досить типовим, коли клас має лише один такий ініціалізатор. Призначені ініціалізатори є свого роду єдиним вікном, в якому відбувається ініціалізація, і через яке процес ініціалізації переходить далі по ланцюжку батьківських класів.

Кожен клас повинен мати як мінімум один призначений ініціалізатор. В деяких випадках, ця вимога задовольняється через наслідування одного чи більше призначених ініціалізаторів з батьківського класу, як описано далі в підрозділі Автоматичне наслідування ініціалізаторів.

Ініціалізатори для зручності є вторинними, підтримуючими ініціалізаторами класу. Можна визначити ініціалізатор для зручності, що викликає призначений ініціалізатор цього ж класу, передаючи в якості його параметрів значення за замовчанням. Можна також оголосити ініціалізатор для зручності, що створює екземпляр класу для специфічного випадку або вхідного значення.

Ініціалізатори для зручності не є обов’язковими: не слід їх створювати, якщо клас їх не потребує. Слід створювати ініціалізатори для зручності, як скорочення синтаксису для поширеного шаблону ініціалізації, і тим само економити час, або для того, щоб робити ініціалізацію класу зрозумілішою.

Синтаксис призначених ініціалізаторів та ініціалізаторів для зручності

Призначені ініціалізатори класів записуються так само, як і прості ініціалізатори типів-значень:

init(параметри) {
    інструкції
}

Ініціалізатори для зручності записуються аналогічним чином, але перед ключовим словом init через пробіл вказується модифікатор convenience:

convenience init(параметри) {
    інструкції
}

Делегування ініціалізації у класах

Для спрощення зв’язків між призначеними ініціалізаторами та ініціалізаторами для зручності, у Swift є три наступні правила делегування викликів ініціалізаторів:

Правило 1

  Призначений ініціалізатор повинен викликати призначений ініціалізатор свого прямого батьківського класу.

Правило 2

  Ініціалізатор для зручності повинен викликати інший ініціалізатор цього ж класу.

Правило 3

  Ініціалізатор для зручності повинен зрештою викликати призначений ініціалізатор.

Ось простіший спосіб запам’ятати ці правила:

  • Призначені ініціалізатори завжди делегують нагору
  • Ініціалізатори для зручності завжди делегують на рівні свого класу

Дані правила проілюстровані на зображені нижче:

Тут, батьківський клас має єдиний призначений ініціалізатор та два ініціалізатори для зручності. Один з ініціалізаторів для зручності викликає інший ініціалізатор для зручності, а той у свою чергу викликає єдиний призначений ініціалізатор. Це задовольняє вищезазначені правила 2 та 3. Батьківський клас не має власного батьківського класу, тому правило 1 не застосовується.

Клас-нащадок на зображенні має два призначені ініціалізатори та один ініціалізатор для зручності. Ініціалізатор для зручності повинен викликати один з двох призначених ініціалізаторів, бо він може викликати лише ініціалізатори зі свого ж класу. Це задовольняє вищезазначеним правилам 2 та 3. Обидва призначені ініціалізатори повинні викликати єдиний призначений ініціалізатор батьківського класу, що задовольняє вищезазначене правило 1.

Примітка

Дані правила не впливають на те, як користувачі вашого класу створюють екземпляри кожного класу. Будь-який ініціалізатор на діаграмі вище може використатись для створення повінстю проініціалізованого екземпляру відповідного класу. Ці правила впливають лише на те, як ви пишете реалізацію ініціалізаторів класу.

Зображення нижче демонструє складнішу ієрархію з чотирьох класів. Вона ілюструє, як призначені ініціалізатори в ієрархії працюють як “єдині вікна” в ініціалізації класу, спрощуючи відносини між класами в ланцюжку наслідування:


Двофазна ініціалізація

Ініціалізація класів у Swift є двофазним процесом. У ході першої фази кожній властивості, що зберігається, присвоюється початкове значення класом, в якому її оголошено. Як тільки було визначено початковий стан кожної властивості, що зберігається, розпочинається друга фаза, і кожному класу дається можливість підлаштувати його властивості, що зберігаються, перед початком використання нового екземпляру.

Використання двофазного процесу ініціалізації робить ініціалізацію безпечною, при тому надаючи повну гнучкість для кожного класу в ієрархії класів. Двофазна ініціалізація запобігає доступу до значень властивостей до їх ініціалізації, та запобігає неочікуваному присвоєнню значення властивості в іншому ініціалізаторі.

Примітка

Процес двофазної ініціалізації у Swift є аналогічним до ініціалізації в Objective-C. Основною відмінністю є те, що під час фази 1, Objective-C присвоює нульові значення (такі як 0 або nil) кожній властивості. Процес ініціалізації у Swift є гнучкішим, оскільки від дає вам можливість присвоювати власні початкові значення, та працює з типами, для яких 0 або nil не може бути валідним початковим значенням.

Компілятор Swift проводить чотири корисні перевірки безпеки, що убезпечують двофазну ініціалізацію від помилок:

Перевірка безпеки 1

  Призначений ініціалізатор повинен гарантувати, що всі властивості, котрі вводяться даним класом, було проініціалізовано перед делегуванням ініціалізації до батьківського класу.

Як зазначено вище, пам’ять для об’єкта вважається повністю проініціалізованою лише після того, як початковий стан усіх його властивостей, що зберігаються, є відомим. Для того щоб це правило задовольнялось, призначений ініціалізатор повинен гарантувати, що всі власні властивості даного класу було проініціалізовано перед рухом вгору по ланцюгу ініціалізаторів.

Перевірка безпеки 2

  Призначений ініціалізатор повинен делегувати ініціалізацію до батьківського класу перед присвоєнням значення до успадкованої властивості. Інакше, присвоєне цим ініціалізатором значення завжди буде перезаписане батьківським класом в ході його власної ініціалізації.

Перевірка безпеки 3

  Ініціалізатор для зручності повинен делегувати ініціалізацію до іншого ініціалізатора перед присвоєнням значення до будь-якої властивості, в тому числі оголошеної в цьому ж класі. Інакше, присвоєне цим ініціалізатором значення буде перезаписуватись призначеним ініціалізатором цього ж класу.

Перевірка безпеки 4

  Ініціалізатор не може викликати методів екземпляру, звертатись до значень будь-яких властивостей екземпляру, або звертатись до значення `self` до завершення першої фази ініціалізації.

Екземпляр класу не є повністю валідним до кінця першої фази ініціалізації. Звертатись до властивостей та викликати методи на екземплярі класу можна лише після того, як відомо, що даний екземпляр класу є валідним наприкінці першої фази ініціалізації.

Ось як працює двофазна ініціалізація, базуючись на цих чотирьох перевірках безпеки:

Фаза 1

  Призначений ініціалізатор чи ініціалізатор для зручності було викликано на класі.

  Було виділено пам'ять для нового екземпляру класу. Пам'ять ще не було ініціалізовано.

  Призначений ініціалізатор даного класу ініціалізує всі введені в цьому класі властивості, що зберігаються. Пам'ять для цих властивостей тепер проініціалізована.

  Призначений ініціалізатор передає контроль до ініціалізатору батьківського класу, котрий зробить таку ж роботу із його власними властивостями, що зберігаються. Це продовжується по ланцюжку наслідування допоки не буде досягнуто вершини цього ланцюга.

  Як тільки було досягнуто вершину ланцюжка наслідування, і останній клас у ланцюжку проініціалізував всі свої властивості, що зберігаються, пам'ять екземпляру вважається повністю проініціалізованою, і фаза 1 завершується.

Фаза 2

  Йде робота назад по ланцюжоку наслідування починаючи з його вершини, кожному призначеному ініціалізатору надається можливість подальшого налаштування екземпляру. Ініціалізатор тепер може звертатись до `self` та змінювати його властивості, викликати методи, і так далі.

  Врешті-решт, кожному ініціалізатору для зручності дається можливість налаштувати екземляр для роботи, звертаючись до `self`.

Ось так виглядає фаза 1 для виклику ініціалізації для гіпотетичних класу-нащадку та батьківського класу:



У цьому прикладі, ініціалізація розпочинається викликом ініціалізатора для зручності на класі-нащадку. Цей ініціалізатор для зручності ще не може модифікувати будь-яких властивостей. Він делегує ініціалізацію до призначеного ініціалізатора для цього ж класу.

Призначений ініціалізатор впевнюється в тому, що всі властивості цього класу-нащадку мають значення, відповідно до перевірки безпеки 1. Після цього він викликає призначений ініціалізатор батьківського класу для продовження ініціалізації по ланцюжку.

Призначений ініціалізатор батьківського класу впевнюється в тому, що всі властивості батьківського класу мають значення. Цей клас є вершиною ланцюжка, тому подальше делегування непотрібне.

Як тільки всі властивості батьківського класу мають початкове значення, його пам’ять вважається повністю проініціалізованою, і фаза 1 завершується.

Ось так виглядає фаза 2 для цього ж виклику ініціалізації:



Призначений ініціалізатор батьківського класу тепер має можливість подальшого налаштування екземпляру (хоч і не обов’язково повинен це робити).

Як тільки призначений ініціалізатор батьківського класу завершив свою роботу, призначений ініціалізатор класу-нащадка може провести додаткове налаштування екземпляру (хоч, знову, не зобов’язаний це робити).

Врешті-решт, як тільки призначений ініціалізатор класу-нащадка завершив роботу, ініціалізатор для зручності, котрий було викликано на самому початку, може виконати додаткове налаштування.

Наслідування ініціалізаторів та їх заміщення

На відміну від класів-нащадків у Objective-C, у Swift класи-нащадки не наслідують ініціалізатори батьківського класу за замовчанням. Підхід мови Swift запобігає ситуації, в якій простий ініціалізатор батьківського класу наслідується більш спеціалізованим класом-нащадком, та використовується для створення екземпляру класу-нащадка, що не є повністю чи коректно ініціалізованим.

Примітка

Ініціалізатори батьківських класів наслідуються за певних умов, але тільки тоді, коли це безпечно й доречно. Детальніше з цим можна ознайомитись у підрозділі Автоматичне наслідування ініціалізаторів нижче.

Якщо потрібно, щоб певний клас-нащадок містив один чи більше ініціалізаторів його батьківського класу, слід надати власну реалізацію цих ініціалізаторів всередині класу-нащадку.

Якщо в клас-нащадок додати ініціалізатор, що відповідає призначеному ініціалізатору батьківського класу, фактично відбудеться заміщення цього призначеного ініціалізатору. Тому слід додавати модифікатор override перед оголошенням ініціалізатора класу-нащадка. Це правило працює в тому числі й при заміщенні автоматично створених ініціалізаторів батьківського класу, які описані в підрозділі Ініціалізатори за замовчанням.

Як і з заміщеними властивостями, методами чи індексами, наявність модифікатора override підказує компілятору Swift перевірити, що батьківський клас має відповідний призначений ініціалізатор, та впевнитись, що параметри даного ініціалізатора відповідають параметрам батьківського.

Примітка

Слід завжди вказувати модифікатор override при заміщенні призначеного ініціалізатора батьківського класу, навіть, якщо реалізація класу-нащадка є ініціалізатором для зручності.

І навпаки, якщо ініціалізатор класу-нащадка співпадає з ініціалізатором для зручності батьківського класу, ініціалізатор для зручності батьківського класу не можна прямо викликати у класі-нащадку, відповідно до правил, описаних вище в підрозділі Делегування ініціалізації у класах. Таким чином, строго кажучи, ініціалізатор класу-нащадка не заміщує ініціалізатор батьківського класу. Як результат, при реалізації ініціалізатора для зручності в класі-нащадку, що співпадає з ініціалізатором для зручності батьківського класу, не потрібно писати ключове слово override.

У прикладі нижче визначено базовий клас на ім’я Vehicle, що моделює транспортний засіб. У цьому базовому класі оголошено властивість, що зберігається, на ім’я numberOfWheels (кількість коліс), зі значенням типу Int, що за замовчанням дорівнює 0. Властивість numberOfWheels використовується властивістю, що обчислюється на ім’я description типу String, котра створює опис характеристик даного транспортного засобу:

class Vehicle {
    var numberOfWheels = 0
    var description: String {
        return "\(numberOfWheels) колеса/коліс"
    }
}

У класі Vehicle надано значення за замовчанням для його властивості, що зберігається, і немає явних ініціалізаторів. Як результат, він автоматично отримує ініціалізатор за замовчанням, як описано в підрозділі Ініціалізатори за замовчанням. Ініціалізатор за замовчанням (якщо він доступний) є завжди призначеним ініціалізатором для класу, і може бути використаним для створення нового екземпляру Vehicle із кількістю коліс numberOfWheels, що дорівнює 0:

let vehicle = Vehicle()
print("Транспорт: \(vehicle.description)")
// Транспорт: 0 колеса/коліс

Наступний приклад ілюструє клас-нащадок класу Vehicle на ім’я Bicycle:

class Bicycle: Vehicle {
    override init() {
        super.init()
        numberOfWheels = 2
    }
}

У класі-нащадку Bicycle оголошується власний призначений ініціалізатор, init(). Цей призначений ініціалізатор співпадає з призначеним ініціалізатором батьківського класу Vehicle, і тому версію init() класу Bicycle позначено модифікатором override.

Ініціалізатор init() класу Bicycle починається з виклику super.init(), котрий викликає ініціалізатор за замовчанням батьківського класу Vehicle класу Bicycle. Це дозволяє гарантувати, що успадковану властивість numberOfWheels буде проініціалізовано класом Vehicle перед тим, як клас Bicycle матиме можливість змінити цю властивість. Після виклику super.init(), оригінальне значення властивості numberOfWheels замінюється новим значенням 2.

Якщо створити екземпляр класу Bicycle, можна викликати успадковану ним властивість, що зберігається description, щоб побачити, як оновилось значення властивості numberOfWheels:

let bicycle = Bicycle()
print("Велосипед: \(bicycle.description)")
// Велосипед: 2 колеса/коліс

Примітка

Класи-нащадки під час ініціалізації можуть змінювати лише успадковані змінні властивості, вони не можуть змінювати успадковані константні властивості.

Автоматичне наслідування ініціалізаторів

Як зазначено вище, класи-нащадки не успадковують ініціалізатори своїх базових класів за замовчанням. Однак, ініціалізатори батьківських класів автоматично наслідуються за певних умов. На практиці, це означає, що вам не потрібно писати заміщення ініціалізаторів у багатьох загальних сценаріях; ініціалізатори батьківських класів можуть бути успадкованими з мінімальними зусиллями завжди, коли це безпечно.

Якщо всі нові властивості, що оголошуються у класах нащадках, мають значення за замовчанням, працюють наступні два правила:
Assuming that you provide default values for any new properties you introduce in a subclass, the following two rules apply:

Правило 1

  Якщо клас-нащадок не визначає нових призначених ініціалізаторів, він автоматично успадковує усі призначені ініціалізатори батьківського класу.

Правило 2

  Якщо клас-нащадок має реалізації _усім_ призначеним ініціалізаторам батьківського класу – як шляхом їх наслідування за правилом 1, так і шляхом власної їх реалізації – тоді він автоматично успадковує всі ініціалізатори для зручності батьківського класу.

Ці правила працюють в тому числі й тоді, коли клас-нащадок оголошує додаткові ініціалізатори для зручності.

Примітка

Клас-нащадок може реалізувати призначений ініціалізатор батьківського класу як ініціалізатор для зручності, і таким чимон задовольнити правило 2.

Призначені ініціалізатори та ініціалізатори для зручності в дії

Наступний приклад демонструє призначені ініціалізатори, ініціалізатори для зручності та автоматичне наслідування ініціалізаторів у дії. Цей приклад визначає ієрархію з трьох класів: Food, RecipeIngredient, та ShoppingListItem, і демонструє взаємодію їх ініціалізаторів.

Базовий клас в ієрархії називається Food (Їжа), він є простим класом для інкапсуляції імені харчового продукту. Клас Food вводить єдину властивість типу String на ім’я name, і має два ініціалізатори для створення екземплярів Food:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Без імені]")
    }
}

Зображення нижче демонструє ланцюжок ініціалізаторів класу Food:



Класи не мають почленних ініціалізаторів за замовчанням, і тому в класі Food явно міститься призначений ініціалізатор, що приймає єдиний аргумент на ім’я name. Цей ініціалізатор може використовуватись для створення нового екземпляру класу Food із певним іменем:

let namedMeat = Food(name: "Салечко")
// ім'я namedMeat – "Салечко"

Ініціалізатор init(name: String) класу Food визначено як призначений ініціалізатор, бо він впевнюється в тому, що всі властивості, що зберігаються в новому екземплярі класу Food було проініціалізовано. Клас Food не має батьківського класу, і тому ініціалізатор init(name: String) не повинен викликати super.init() для завершення ініціалізації.

Клас Food також має ініціалізатор для зручності, init(), без аргументів. Ініціалізатор init() надає тимчасове ім’я за замовчанням новим екземплярам класу Food, делегуючи ініціалізацію до ініціалізатора init(name: String), зі значенням аргументу name"[Без імені]":

let mysteryMeat = Food()
// ім'я mysteryMeat – "[Без імені]"

Другим класом в ієрархії є нащадок класу Food на ім’я RecipeIngredient. Клас RecipeIngredient моделює інгредієнт у кухонному рецепті. Він додає властивість типу Int на ім’я quantity, що виражає кількість продукту (на додачу до властивості name, що вже успадкована з класу Food). Він також має два ініціалізатори для створення екземплярів RecipeIngredient:

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

Зображення нижче демонструє ланцюжок ініціалізаторів класу RecipeIngredient:



Клас RecipeIngredient має один призначений ініціалізатор, init(name: String, quantity: Int), котрий можна використовувати для заповнення всіх властивостей нового екземпляру RecipeIngredient значеннями. Цей ініціалізатор починається із присвоєння значення переданого аргументу quantity до однойменної властивості, котра є єдиною новою властивістю, введеною класом RecipeIngredient. Після цього, ініціалізатор делегує ініціалізацію до ініціалізатора init(name: String) класу Food. Цей процес задовольняє перевірку безпеки 1 з підрозділу Двофазна ініціалізація вище.

Клас RecipeIngredient також має ініціалізатор для зручності, init(name: String), котрий можна використовувати для створення екземплярів RecipeIngredient лише за іменем. Цей ініціалізатор для зручності припускає, що кількість інгредієнту дорівнює 1 для будь-якого екземпляру RecipeIngredient, що створюється без явного вказування кількості. Визначення ініціалізатора для зручності робить створення екземплярів RecipeIngredient швидшим та зручнішим, та запобігає дублюванню коду при створенні кількох екземплярів RecipeIngredient з кількістю 1. Цей ініціалізатор просто делегує ініціалізацію призначеному ініціалізатору даного класу, передаючи 1 як значення quantity.

Ініціалізатор для зручності init(name: String) класу RecipeIngredient приймає такі ж аргументи, як і призначений ініціалізатор класу Food. Оскільки цей ініціалізатор для зручності заміщує призначений ініціалізатор батьківського класу, він повинен позначатись ключовим словом override (як описано у підрозділі Наслідування ініціалізаторів та їх заміщення).

Хоч клас RecipeIngredient реалізовує ініціалізатор init(name: String) як ініціалізатор для зручності, цей клас тим не менш реалізовує всі призначені ініціалізатори батьківського класу. Таким чином, клас RecipeIngredient автоматично отримує всі ініціалізатори для зручності свого батьківського класу.

У даному прикладі, батьківським класом класу RecipeIngredient є клас Food, котрий має єдиний ініціалізатор для зручності на ім’я init(). Цей ініціалізатор успадковується класом RecipeIngredient. Успадкована версія ініціалізатора init() працює точно так само як і в класі Food, крім того, що вона делегує ініціалізацію до версії ініціалізатора init(name: String) класу RecipeIngredient, а не класу Food.

Усі три ці ініціалізатори можна використовувати для створення нових екземплярів класу RecipeIngredient:

let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Салечко")
let sixEggs = RecipeIngredient(name: "Яйця", quantity: 6)

Третім та останнім класом в ієрархії є нащадок класу RecipeIngredient на ім’я ShoppingListItem. Клас ShoppingListItem моделює інгредієнт рецепту при його появі в списку покупок.

Кожен елемент у списку покупок спершу є “не купленим”. Щоб відобразити цей факт, клас ShoppingListItem вводить булеву властивість на ім’я purchased, зі значенням за замовчанням false. Клас ShoppingListItem також вводить властивість, що обчислюється description, котра генерує текстовий опис екземпляру ShoppingListItem:

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

Примітка

Клас ShoppingListItem не визначає ініціалізатора для задавання початкового значення властивості purchased, оскільки усі елементи у списку покупок спершу не є купленими.

Оскільки клас ShoppingListItem задає значення за замовчанням усім властивостям, які він вводить, і не визначає жодних власних ініціалізаторі, він автоматично успадковує усі призначені ініціалізатори та ініціалізатори для зручності від свого батьківського класу.

Зображення нижче демонструє повний ланцюжок ініціалізаторів усіх трьох класів:



Для створення нових екземплярів класу ShoppingListItem можна використовувати усі три успадковані ініціалізатори:

var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Салечко"),
    ShoppingListItem(name: "Яйця", quantity: 6),
]
breakfastList[0].name = "Апельсиновий сік"
breakfastList[0].purchased = true
for item in breakfastList {
    print(item.description)
}
// 1 x Апельсиновий сік ✔
// 1 x Салечко ✘
// 6 x Яйця ✘

Тут у прикладі вище з літерала масиву, що містить три нові екземпляри класу ShoppingListItem, створюється масив на ім’я breakfastList. Тип даного масиву визначено як [ShoppingListItem]. Після створення масиву, назву першого в масиві екземпляру ShoppingListItem змінено з "[Без імені]" на "Апельсиновий сік", а потім цей екземпляр помічається як куплений. Далі слідує друк описів кожного елементу з масиву, що демонструє те, що задавання початкових станів відбулось так, як очікувалось.

Ненадійні ініціалізатори

Іноді буває корисним мати клас, структуру чи перечислення, чия ініціалізація може провалитись. Цей провал може бути спричиненим некоректними значеннями параметрів ініціалізації, відсутністю необхідного зовнішнього ресурсу, або іншою умовою, що перешкоджає успішній ініціалізації.

Щоб впоратись з умовами ініціалізації, що призводять до її провалу, слід визначити один або декілька ненадійних ініціалізаторів в оголошенні класу, структури чи перечислення. Ненадійні ініціалізатори записуються за допомогою додавання знаку питання після ключового слова init (init?).

Примітка

Не можна оголошувати одночасно звичайний та ненадійний ініціалізатори з однаковими типами параметрів та їх іменами.

Ненадійний ініціалізатор створює опціональне значення типу, котрий ініціалізується. Щоб відобразити момент провалу ініціалізації, слід писати return nil всередині ненадійного ініціалізатора.

Примітка

Строго кажучи, ініціалізатори не повертають значення. Натомість їхня роль полягає у тому, щоб упевнитись, що self є повністю та коректно проініціалізовано до моменту завершення ініціалізації. Хоча провали ініціалізаці й записуються як return nil, не потрібно писати ключове слово return для позначення успішного завершення ініціалізації.

У прикладі нижче визначено структуру Animal (що моделює тварину), із константною властивістю типу String на ім’я species (що моделює її вид). У структурі Animal також визначено ненадійний ініціалізатор з єдиним параметром на ім’я species. Цей ініціалізатор перевіряє передане значення параметру species: якщо це порожній рядок, генерується провал ініціалізації. В іншому випадку, значення присвоюється властивості species, та ініціалізація завершується вдало:

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

Тепер можна скористатись ненадійним ініціалізатором, щоб спробувати створити новий екземпляр Animal та перевірити, чи була ініціалізація успішною:

let someCreature = Animal(species: "Жирафа")
// someCreature має тип Animal?, не Animal

if let giraffe = someCreature {
    print("Тварина було ініціалізовани з видом \(giraffe.species)")
}
// Надрукує "Тварина було ініціалізовани з видом Жирафа"

Якщо передати порожній рядок як параметр species ненадійного ініціалізатора, це спровокує провал ініціалізації:

let anonymousCreature = Animal(species: "")
// anonymousCreature має тип Animal?, не Animal

if anonymousCreature == nil {
    print("Неможливо ініціалізувати анонімне створіння")
}
// Надрукує "Неможливо ініціалізувати анонімне створіння"

Примітка

Перевірка рядка на порожність (наприклад, "" замістю "Жирафа") – це не те ж саме, що перевірка на nil для визначення відсутності значення опціонального значення типу String. У прикладі вище, порожній рядок ("") є дійсним, неопціональним значенням типу String. Однак, для трарини мати порожній рядок в якості властивості species (вид) є недоречним. Щоб змоделювати це обмеження, ненадійний ініціалізатор провалює ініціалізацію у випадку виявлення порожнього рядка.

Ненадійні ініціалізатори в перечисленнях

Ненадійними ініціалізаторами зручно користуватись, щоб обрати відповідний елемент перечислення, базуючись на одному або кількох параметрах. Ініціалізатор може провалитись, якщо заданим параметрам не відповідає жоден з елементів перечислення.

У прикладі нижче визначено перечислення TemperatureUnit, котре моделює одиницю вимірювання температури та має три елементи (kelvin, celsius, та fahrenheit). Ненадійний ініціалізатор використовується для знаходження елементу перечислення за значенням типу Character, що представляє символ температури відповідної шкали:

enum TemperatureUnit {
    case kelvin, celsius, fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .kelvin
        case "C":
            self = .celsius
        case "F":
            self = .fahrenheit
        default:
            return nil
        }
    }
}

Можна використовувати цей ненадійний ініціалізатор для того, щоб обрати відповідний елемент перечислення, або провалитись, якщо параметр не відповідає жодному з елементів перечислення:

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("Це відома шкала температури, тому ініціалізація була успішною.")
}
// Надрукує "Це відома шкала температури, тому ініціалізація була успішною."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("Це невідома шкала температури, тому ініціалізація провалилась.")
}
// Надрукує "Це невідома шкала температури, тому ініціалізація провалилась."

Ненадійні ініціалізатори в перечисленнях з сирими значеннями

Перечислення із сирими значеннями автоматично отримують ненадійний ініціалізатор, init?(rawValue:), котрий приймає параметр на ім’я rawValue відповідного типу сирого значення, та обирає відповідний елемент перечислення по сирому значенню, або провалюється у випадку, якщо не існує відповідного елементу.

Можна переписати приклад з перечисленням TemperatureUnit вище, використовуючи сирі значенням типу Character та скориставшись перевагами ініціалізатора init?(rawValue:):

enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("Це відома шкала температури, тому ініціалізація була успішною.")
}
// Надрукує "Це відома шкала температури, тому ініціалізація була успішною."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("Це невідома шкала температури, тому ініціалізація провалилась.")
}
// Надрукує "Це невідома шкала температури, тому ініціалізація провалилась."

Поширення провалу ініціалізації

Ненадійний ініціалізатор класу, структури чи перечислення може делегувати ініціалізацію до іншого ненадійного ініціалізатора цього ж класу, структури чи перечислення. Аналогічно, ненадійний ініціалізатор класу-нащадка може делегувати ініціалізацію до ненадійного ініціалізатора батьківського класу.

В будь-якому випадку, якщо відбувається делегування ініціалізатору, в котрому ініціалізація провалюється, весь процес ініціалізації одразу провалюється, і жоден подальший код ініціалізації не виконується.

Примітка

Ненадійний ініціалізатор також може делегувати ініціалізацію до звичайного ініціалізатора. Слід користуватись таким підоходом для того, щоб розширити процес ініціалізації, що не провалюється, додатковою обробкою провалів.

У прикладі нижче оголошено клас-нащадок класу Product, що називається CartItem. Клас CartItem моделює елемент в кошику інтернет-магазину. CartItem додає константну властивість, що зберігається, на ім’я quantity, та має ненадійний ініціалізатор, що контролює, що ця властивість завжди має значення не менше за 1:

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

Ненадійний ініціалізатор класу CartItem розпочинається з валідації аргументу quantity. Якщо значення quantity менше за 1, тобто не валідне, весь процес ініціалізації одразу провалюється і подальший код ініціалізації не виконується. Аналогічно, ненадійний ініціалізатор класу Product перевіряє значення name, і якщо це порожній рядок – процес ініціалізації одразу провалиться.

Якщо створити екземпляр CartItem із непорожньою назвою та кількістю 1 або більше, процес ініціалізації завершується успішно:

if let twoSocks = CartItem(name: "шкарпетка", quantity: 2) {
    print("Товар: \(twoSocks.name), кількість: \(twoSocks.quantity)")
}
// Надрукує "Товар: шкарпетка, кількість: 2"

Якщо створити екземпляр класу CartItem зі значенням quantity, що дорівнює 0, ініціалізатор CartItem провалить ініціалізацію:

if let zeroShirts = CartItem(name: "сорочка", quantity: 0) {
    print("Товар: \(zeroShirts.name), кількість: \(zeroShirts.quantity)")
} else {
    print("Неможливо ініціалізувати нуль сорочок")
}
// Надрукує "Неможливо ініціалізувати нуль сорочок"

Аналогічно, якщо спробувати створити екземпляр класу CartItem з порожнім значенням name, ініціалізатор батьківського класу Product провалить ініціалізацію:

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Товар: \(oneUnnamed.name), кількість: \(oneUnnamed.quantity)")
} else {
    print("Неможливо ініціалізувати безіменний товар")
}
// Надрукує "Неможливо ініціалізувати безіменний товар"

Заміщення ненадійних ініціалізаторів

Ненадійні ініціалізатори батьківських класів можна заміщувати в класах-нащадках, як і будь-які інші ініціалізатори. Також можливо замістити ненадійний ініціалізатор батьківського класу надійним ініціалізатором класу-нащадка. Це дає можливість створювати класи-нащадки, чия ініціалізація не провалюється, хоч ініціалізація батьківського класу і може при цьому провалюватись.

Слід помітити, що коли заміщується ненадійний ініціалізатор батьківського класу надійним ініціалізатором класу-нащадка, єдиним способом делегувати ініціалізацію до батьківського класу є примусове розгортання результату виклику ненадійного ініціалізатора батьківського класу.

Примітка

Можна замістити ненадійний ініціалізатор надійним, але не можна зробити навпаки.

У прикладі нижче оголошено клас на ім’я Document. Цей клас моделює документ, котрий може бути ініціалізованим властивістю name, котра має бути або непорожнім рядком, або nil, однак не може бути порожнім рядком:

class Document {
    var name: String?
    // цей ініціалізатор створює документ зі значенням name nil
    init() {}
    // цей ініціалізатор створює документ із непорожнім значенням name
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

У наступному прикладі оголошено клас-нащадок класу Document на ім’я AutomaticallyNamedDocument. Клас-нащадок AutomaticallyNamedDocument заміщує обидва призначених ініціалізатори, введених класом Document. Ці заміщення гарантують, що екземпляр класу AutomaticallyNamedDocument матиме початкове значення name "[Безіменний]" у випадках, якщо він ініціалізується без назви, або якщо до ініціалізатора init(name:) передано порожній рядок:

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Безіменний]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Безіменний]"
        } else {
            self.name = name
        }
    }
}

Клас AutomaticallyNamedDocument заміщує ненадійний ініціалізатор батьківського класу init?(name:) надійним ініціалізатором init(name:). Оскільки клас AutomaticallyNamedDocument справляється з випадком порожнього рядка іншим способом, аніж його батьківський клас, його ініціалізатору не потрібно провалюватись, і тому даний клас натомість містить надійну версію даного ініціалізатора.

В реалізації надійного ініціалізатора класу-нащадка можна використовувати примусове розгортання виклику ненадійного ініціалізатора батьківського класу. Наприклад, клас-нащадок UntitledDocument у прикладі нижче завжди має назву "[Безіменний]", і використовує під час ініціалізації ненадійний ініціалізатор батьківського класу init(name:):

class UntitledDocument: Document {
    override init() {
        super.init(name: "[Безіменний]")!
    }
}

В даному випадку, якби ініціалізатор init(name:) батьківського класу був викликаний з порожнім рядком в якості параметра name, операція примусового розгортання призвела б до помилки часу виконання. Однак, оскільки в якості параметра завжди передається рядкова константа, даний ініціалізатор не може провалитись і в даному випадку не буде помилки часу виконання.

Ненадійний ініціалізатор init!

Як привило, ненадійні ініціалізатори оголошують за допомогою знаку питання після ключового слова init (init?), і такі ініціалізатори створюють опціональний екземпляр відповідного типу. Однак, також можна оголошувати ненадійні ініціалізатори, котрі повертають опціональні екземпляри відповідного типу, що розгортаються неявно. Такі ініціалізатори записуються за допомогою знаку оклику після ключового слова init (init!) замість знаку питання.

Можна делегувати ініціалізацію з ініціалізатора init? до init! і навпаки, а також можна заміщувати ініціалізатор init? ініціалізатором init! і навпаки. Також можна делегувати з ініціалізатора init до init!, однак це спричинить помилку часу виконання у випадку, якщо ініціалізатор init! спровокує провал ініціалізації.

Обов’язкові ініціалізатори

Якщо перед оголошенням ініціалізатору класу вказати ключове слово required, усі нащадки даного класу повинні будуть реалізовувати даний ініціалізатор:

class SomeClass {
    required init() {
        // ініціалізація буде проходити тут
    }
}

Також обов’язково слід вказувати модифікатор required перед кожною реалізацією обов’язкового ініціалізатора в класі-нащадку, щоб позначити, що обов’язковість даного ініціалізатора стосується також подальших нащадків у ланцюжку. Перед обов’язковим заміщенням призначеного ініціалізатора при цьому не обов’язково вказувати ключове слово override:

class SomeSubclass: SomeClass {
    required init() {
        // реалізація обов'язкового ініціалізатора класу-нащада буде проходити тут
    }
}

Примітка

Не обов’язково надавати явну реалізацію обов’язкового ініціалізатора, якщо можна делегувати ініціалізацію успадкованому ініціалізатору.

Присвоєння початкового значення за допомогою замикання чи функції

Якщо початкове значення властивості, що зберігається, потребує якогось налаштування, для генерації такого початкового значення можна використати замикання чи глобальну функцію. При ініціалізації нового екземпляру типу, до якого належить властивість, буде викликано замикання чи функція, і її значення, що повертається, буде присвоєно властивості як початкове значення.

Ці види замикань чи функцій, як правило, створюють тимчасове значення того ж типу, що й властивість, конфігурують його до бажаного початкового стану, а потім повертають це тимчасове значення для використання як початкове значення властивості.

Ось приблизний шаблон використання замикання для створення початкового значення властивості:

class SomeClass {
    let someProperty: SomeType = {
        // Значення за замовчанням властивості someProperty
        // створюється в цьому замиканні.
        // someValue повинно мати тип SomeType.
        return someValue
    }()
}

Слід помітити, що одразу за фігурними дужками замикання йде порожня пара круглих дужок. Це вказує Swift виконати замикання одразу після його оголошення. Якщо пропустити ці круглі дужки, властивості буде присвоюватись саме замикання, а не значення, яке повертає замикання.

Примітка

При використанні замикань для ініціалізації властивостей, слід пам’ятати, що решта екземпляру ще не було ініціалізовано на момент виконання замикання. Це означає, що не можна звертатись до інших властивостей з цього замикання, навіть якщо вони мають значення за за мовчанням. Також не можна використовувати неявну властивість self, чи викликати будь-які методи екземпляру.

У прикладі нижче оголошено структуру з назвою Chessboard, котра моделює дошку для гри в шахи. В шахи грають на дошці розміром 8 x 8 клітинок, котрі почергово зафарбовані в білий та чорний.


Щоб представити дану ігрову дошку, структура Chessboard має одну властивість на ім’я boardColors, котра є масивом з 64 булевих значень. Значення true в цьому масиві представляє чорну клітинку, а значення false – білу клітинку. Перший елемент в масиві представляє верхню ліву клітинку на дошці, а останній елемент в масиві – нижню праву.

Масив boardColors ініціалізується за допомогою замикання для налаштування значень кольорів клітинок:

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

Щоразу при створенні екземпляру Chessboard, буде виконуватись замикання, котре обчислюватиме й повертатиме початкове значення властивості boardColors. Дане замикання у прикладі вище обчислює та присвоює відповідний колір кожній клітинці на дошці в тимчасово створеному масиві temporaryBoard, і повертає цей тимчасовий масив після завершення налаштування. Масив, що повертається, буде збережено у властивості boardColors та потім до нього можна звертатись у допоміжній функції squareIsBlackAtRow, котра перевіряє колір клітинки за індексами:

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Надрукує "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Надрукує "false"