1. Навіщо потрібна ініціалізація
Ініціалізація — це момент, коли обʼєкт класу «народжується» і має одразу опинитися в коректному, придатному до роботи стані. Можна уявити це як чек-лист пілота: перш ніж злітати, усі тумблери мають бути у зрозумілих позиціях. Якщо якась властивість, що зберігається не отримала значення, обʼєкт фактично «недозібраний», і Swift, на щастя, не дозволить вам випадково працювати з таким обʼєктом.
З практичної точки зору init потрібен, щоб:
- задати обовʼязкові властивості (наприклад, id, name, createdAt);
- перевірити вхідні дані та привести їх до нормального вигляду (наприклад, обрізати пробіли);
- створити обʼєкт у такому стані, щоб після let obj = ... можна було впевнено викликати методи й читати властивості.
Важливо: у цій лекції ми говоримо про звичайні класи без успадкування, щоб зосередитися на базових правилах.
2. Усі stored properties мають отримати значення
Найбільш «залізобетонне» правило ініціалізації в Swift звучить так: до моменту завершення init усі stored properties мають бути ініціалізовані. Або властивість має значення за замовчуванням прямо в оголошенні, або ви присвоюєте його в init.
Компілятор перевіряє це дуже суворо. Це частина того, що зазвичай називають definite initialization — «визначена ініціалізація»: Swift намагається довести, що кожен фрагмент стану отримав значення на всіх шляхах виконання. І так, він справді аналізує розгалуження (умови) та свариться, якщо на якомусь шляху ви забули присвоїти властивість.
Цю ідею добре видно навіть на прикладах із делегуванням ініціалізації: якщо ви почали делегувати через self.init(...), то до цього моменту не можна використовувати self, тому що обʼєкт ще «не зібраний».
Приклад: помилка «властивість не ініціалізована»
Псевдокод / схема: приклад навмисно не компілюється, бо показує типову помилку.
class UserProfile {
var name: String
var age: Int
init() {
self.name = "Anonymous"
// self.age не задано → компілятор не пропустить
}
}
Тут age не має значення за замовчуванням і не присвоюється в init. Тому Swift чесно каже: «Я не можу гарантувати, що обʼєкт буде коректним».
Приклад: виправляємо — задаємо обидві властивості
class UserProfile {
var name: String
var age: Int
init() {
self.name = "Anonymous"
self.age = 0
}
}
Тепер обʼєкт щоразу створюється повністю.
3. Значення за замовчуванням vs присвоювання в init
Коли ви проєктуєте клас, у вас майже завжди є вибір: дати властивості значення за замовчуванням або зробити її обовʼязковим параметром init. Новачки часто обирають підхід «давайте все зробимо optional або дамо дефолт» — і отримують обʼєкт, який начебто існує, але сенс у нього туманний. Краще сприймати значення за замовчуванням як усвідомлене рішення: «у 80% випадків значення саме таке».
Нижче — таблиця, яка допомагає обрати підхід.
| Підхід | Як виглядає | Коли підходить | Що може піти не так |
|---|---|---|---|
| Значення за замовчуванням | |
«розумний дефолт» очевидний | можна випадково забути встановити «справжнє» значення |
| Присвоювання в init | |
значення обовʼязкове для коректного стану | потрібно писати більше коду, але це чесна ціна |
Мініприклад: частина властивостей із дефолтом, частина — обовʼязкові
import Foundation
class ReadingProgress {
let bookID: Int
var currentPage: Int = 0
let startedAt: Date = Date()
init(bookID: Int) {
self.bookID = bookID
}
}
Тут bookID обовʼязковий (без нього обʼєкт беззмістовний), а currentPage логічно стартує з 0, startedAt — «прямо зараз».
4. let і self в ініціалізаторі
let-властивості: задаємо один раз
let-властивість у класі — це обіцянка: «після створення обʼєкта це значення не змінюється». І тут є важлива деталь, яка часто збиває з пантелику: let-властивість можна (і треба) присвоїти в init, але після init вона стає незмінною.
Це корисно для ідентифікаторів, дат створення, «конфігурації сесії», тобто для всього, що має залишатися стабільним упродовж життя обʼєкта.
import Foundation
class BookRecord {
let id: Int
let createdAt: Date
var title: String
init(id: Int, title: String) {
self.id = id
self.createdAt = Date()
self.title = title
}
}
Після створення BookRecord ви можете змінити title, але id і createdAt — ні. І це добре: «паспорт книги» не повинен переписуватися через настрій програміста.
Коли потрібен self
Слово self — це посилання на «поточний екземпляр». У методах ми часто можемо писати без self. (Swift це дозволяє), але в ініціалізаторі self особливо важливий у двох ситуаціях: коли потрібно явно показати, що ви присвоюєте властивості, і коли імʼя параметра збігається з імʼям властивості.
Якщо властивість називається name, і параметр теж name, то запис name = name перетворюється на комедію: «присвоїти параметр самому собі». Тому правило просте: за збігу імен пишемо self.name = name.
class Member {
var name: String
init(name: String) {
self.name = name
}
}
А ось коли імена різні, self іноді можна опустити (але в навчальних прикладах я часто залишаю self. для ясності — нехай код буде трохи довшим, зате читати його легше).
class Member {
var name: String
init(initialName: String) {
name = initialName
}
}
Обидва варіанти коректні, але перший зазвичай читається простіше, коли ви лише звикаєте до класів.
Чому self не можна використовувати занадто рано
Момент, який дивує новачків: усередині init не можна робити все підряд із self, поки обʼєкт не повністю ініціалізований. З точки зору компілятора обʼєкт ще не готовий, і ви не маєте права поводитися так, ніби він уже повноцінний.
Інтуїтивна модель така: поки ви не присвоїли значення всім обовʼязковим stored-властивостям, обʼєкт — як ноутбук без установленої операційної системи. Корпус є, клавіатура є, але запускати «Photoshop» зарано.
Звідси випливають практичні правила:
- спочатку присвоюємо значення stored-властивостям;
- лише потім викликаємо методи, які використовують ці властивості;
- і обережно ставимося до логіки, яка намагається використати self «занадто рано».
Приклад: метод викликається після ініціалізації властивостей
class Greeter {
var name: String
init(name: String) {
self.name = name
print("Привіт, \(self.name)!") // Привіт, Ana!
}
}
Приклад: типова помилка «використання до ініціалізації»
Псевдокод / схема: тут важливіша сама ідея, а не компіляція.
class Greeter {
var name: String
init(name: String) {
print(self.name) // не можна: name ще не задано
self.name = name
}
}
Компілятор захищає вас від читання сміття (у буквальному сенсі) з неініціалізованої памʼяті. І це одна з причин, чому Swift так приємно використовувати: він не дає вам «випадково» побудувати обʼєкт-зомбі.
5. Приклад: LibrarySession
Щоб не застрягати в абстракціях, додамо в наш навчальний консольний застосунок (той самий, який з часом перетвориться на LibraryCLI) невеликий клас LibrarySession. Уявімо, що сесія потрібна, щоб зберігати контекст поточного запуску застосунку: хто запустив, коли й скільки команд уже оброблено. Це добрий кандидат на class, тому що сесія — «одна на запуск», і її зручно передавати за посиланням між частинами коду.
Почнемо з простого: у сесії є незмінний sessionID, дата старту і лічильник виконаних команд.
import Foundation
class LibrarySession {
let sessionID: Int
let startedAt: Date
var handledCommands: Int
init(sessionID: Int) {
self.sessionID = sessionID
self.startedAt = Date()
self.handledCommands = 0
}
}
Зверніть увагу на таке поєднання рішень:
sessionID — let, тому що це «номер сесії»; він не має змінюватися.
startedAt — теж let: сесію запускають один раз.
handledCommands — var: кількість команд зростає.
Додамо трохи поведінки й перевіримо, що обʼєкт справді працює
import Foundation
class LibrarySession {
let sessionID: Int
let startedAt: Date
var handledCommands: Int = 0
init(sessionID: Int) {
self.sessionID = sessionID
self.startedAt = Date()
}
func markCommandHandled() {
handledCommands += 1
}
}
Тут ми зробили handledCommands із значенням за замовчуванням 0, і це дозволило прибрати присвоювання з init. Це нормально, якщо значення справді завжди починається з нуля (а воно починається, якщо ви не телепортуєте сесію з майбутнього).
Мінідемо використання
import Foundation
let session = LibrarySession(sessionID: 1)
session.markCommandHandled()
session.markCommandHandled()
print(session.sessionID) // 1
print(session.handledCommands) // 2
У цьому прикладі добре видно, що init задає основу, а методи вже «живуть» поверх неї.
Невелика схема: що має зробити init
Псевдокод / схема:
flowchart TD
A["Виклик LibrarySession(sessionID: ...)"] --> B[init починає роботу]
B --> C[Присвоюємо sessionID]
C --> D[Ініціалізуємо startedAt]
D --> E["handledCommands = 0 (значення за замовчуванням або присвоювання в init)"]
E --> F[Обʼєкт повністю готовий]
F --> G[Можна викликати методи й читати властивості]
Сенс схеми простий: init — це «збірний цех», а не місце для пригод. Чим менше там сюрпризів, тим легше жити.
6. Типові помилки
У цьому розділі зберемо типові граблі, на які наступають майже всі, хто вперше пише init у класах. Це нормально: Swift суворий, зате чесний — він свариться раніше, ніж ви отримаєте загадковий краш у середовищі виконання.
Помилка №1: забули ініціалізувати stored property.
Часто це трапляється, коли ви додали нову властивість у клас, але не оновили init. У голові ви вже «все розумієте», а компілятор — ні: він бачить конкретний список stored-властивостей і вимагає, щоб кожна отримала значення. Лікується просто: додали stored property без дефолту — одразу дописали присвоювання в init.
Помилка №2: спроба змінити let-властивість після створення обʼєкта.
Іноді це виглядає так: «ой, я передумав, нехай id стане іншим». Але let — це не «бажано не змінювати», а «не можна змінювати». Якщо ви ловите себе на бажанні змінювати let, можливо, це не let, а var, або ви неправильно спроєктували модель і ідентифікатор має бути іншим (наприклад, обчислюваним, але це вже інша історія).
Помилка №3: name = name замість self.name = name.
Це класична плутанина імен. У кращому разі компілятор вам допоможе, у гіршому — ви випадково присвоїте параметр самому собі й залишите властивість неініціалізованою (і знову отримаєте помилку компіляції). Звичка писати self. в ініціалізаторах за збігу імен майже миттєво прибирає цю категорію багів.
Помилка №4: занадто раннє використання self до повної ініціалізації.
У новачків це зазвичай виглядає логічно: «я ж у init, отже обʼєкт уже є». Формально так, «є», але Swift вважає, що обʼєкт ще не зобовʼязаний бути коректним, доки ви не задали всі обовʼязкові властивості. Тому будь-які спроби працювати з self до завершення ініціалізації призводять до помилок компіляції. Добрий стиль — спочатку присвоїти всі значення, а потім робити все інше.
Помилка №5: перетворення init на «комбайн»: і валідація, і завантаження даних, і друк логів, і половина бізнес-логіки.
Технічно іноді так зробити можна, але підтримувати це важко. Ініціалізатор має створювати коректний початковий стан, а не виконувати цілий сценарій застосунку. Якщо ви відчуваєте, що init розростається, часто це сигнал: частину логіки варто винести в окремі методи (і викликати їх уже після створення обʼєкта) або переглянути, які властивості справді обовʼязкові на старті.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ