1. convenience init: роль і правила
Коли ви проєктуєте клас, майже завжди виявляється, що користувачеві цього класу — тобто вам же, але «через тиждень», — хочеться створювати обʼєкт по-різному. Іноді у вас є всі дані, іноді — лише частина, іноді потрібен «швидкий старт» із налаштуваннями за замовчуванням, а іноді — «деморежим» із тестовими даними. Якщо на кожен варіант писати окремий повноцінний init із присвоєнням усіх властивостей, код починає розмножуватися, як кролики на курсах зі Swift.
Проблема в тому, що дублювання в ініціалізації особливо неприємне: ви змінюєте правило в одному місці — наприклад, тепер name не може бути порожнім, — а забуваєте оновити другий або третій init. У результаті обʼєкт створюється нібито правильно, але опиняється в неправильному стані. Помилка спливає пізніше, у зовсім іншому місці. convenience init і задуманий як ліки від такого дублювання: один «головний» init задає правила, а інші лише акуратно підводять до нього.
Designated vs convenience: хто за що відповідає
Щоб не сприймати convenience init як «магічне слово, щоб компілювалося», домовмося про ролі. У класі є ініціалізатори двох типів: ті, що справді ініціалізують обʼєкт — призначають значення stored-властивостям і приводять обʼєкт до коректного початкового стану, — і ті, що є альтернативним входом та делегують створення «основному» ініціалізатору.
У класів це розмежування зроблено навмисно й явно: ініціалізатор, який делегує, позначається ключовим словом convenience. Причина в тому, що для класів правила ініціалізації загалом складніші, і Swift вимагає явного маркера: «я делегую, а не ініціалізую все сам».
Зручно тримати в голові таку табличку:
| Вид init | Що робить | Головний обовʼязок |
|---|---|---|
| designated init(...) | «збирає» обʼєкт | гарантувати коректний початковий стан |
| convenience init(...) | «підвозить» аргументи до designated | прибрати дублювання, дати зручний API для створення |
Головне правило: convenience init делегує через self.init(...)
Правило дня, яке рятує від половини помилок: convenience init має викликати інший ініціалізатор цього ж класу через self.init(...). Не «майже завжди», не «зазвичай», а саме має — інакше він не convenience.
Важливе уточнення: до виклику self.init(...) ви можете робити обчислення з локальними змінними, наприклад очищати рядок або парсити число. Але ви не можете звертатися до self і до його властивостей. З погляду компілятора обʼєкт іще «не зібраний», тож чіпати його зарано.
Компілювальний приклад: підготовка даних до self.init
import Foundation
class UserProfile {
let name: String
init(name: String) {
self.name = name
}
convenience init(rawName: String) {
let trimmed = rawName.trimmingCharacters(in: .whitespacesAndNewlines)
let finalName = trimmed.isEmpty ? "Анонімний" : trimmed
self.init(name: finalName)
}
}
let a = UserProfile(rawName: " ")
print(a.name) // Анонімний
Тут convenience init(rawName:) не «ініціалізує» name напряму. Він готує значення й делегує в designated init(name:).
Як працює делегування: ланцюжки і точка зупинки
Коли кажуть «convenience init чіпляється», зазвичай мають на увазі, що ви можете будувати ланцюжок ініціалізаторів, де один зручний вхід делегує іншому, а той — третьому, і так далі. Головне правило: ланцюжок має зрештою дійти до designated-ініціалізатора, який реально задає базові значення.
Корисно уявити це як маршрут у метро: convenience init() — це станція «швидкий вхід», далі ви пересідаєте на convenience init(name:), а потім обовʼязково потрапляєте на кінцеву станцію init(name:books:isDebug:), де обʼєкт справді збирається.
Схема: ланцюжок має завершитися на designated
convenience init()
-> convenience init(name:)
-> designated init(name:books:isDebug:)
Якщо ланцюжок не завершується, можна випадково отримати безкінечну рекурсію: один convenience init викликає інший, а той по колу повертається назад. Це вже не метро, а біг по колу на стадіоні, тільки замість здоровʼя — стек викликів.
2. Практика: LibraryApp і зручні входи
Щоб приклади не були «у вакуумі», давайте продовжимо наш курс-проєкт у дусі консольного застосунку бібліотеки. Ми вже багато разів працювали з колекціями, рядками й командами, тож тепер просто додамо обʼєкт, який зберігає стан застосунку як клас.
Ідея проста: LibraryApp зберігає імʼя бібліотеки, список книг і прапорець «дебаг». Нам потрібен один designated init, який гарантує, що обʼєкт створено коректно, і кілька зручних входів: порожня бібліотека, бібліотека з демо-даними, бібліотека з іменем за замовчуванням.
Компілювальний приклад: модель і designated init
import Foundation
struct Book {
let title: String
}
class LibraryApp {
let name: String
private(set) var books: [Book]
var isDebug: Bool
init(name: String, books: [Book], isDebug: Bool) {
self.name = name
self.books = books
self.isDebug = isDebug
}
}
Це наш «канонічний» ініціалізатор: він отримує все й одразу, і саме він задає початковий стан.
Компілювальний приклад: convenience init(name:) для порожньої бібліотеки
import Foundation
extension LibraryApp {
convenience init(name: String) {
self.init(name: name, books: [], isDebug: false)
}
}
let app = LibraryApp(name: "Домашня бібліотека")
print(app.books.count) // 0
Тут convenience init(name:) — це просто «коротка форма»: імʼя задано, решта — налаштування за замовчуванням.
Компілювальний приклад: convenience init() для швидкого старту
import Foundation
extension LibraryApp {
convenience init() {
self.init(name: "Моя бібліотека")
}
}
let quick = LibraryApp()
print(quick.name) // Моя бібліотека
Зверніть увагу: convenience init() делегує іншому convenience init(name:), а той уже доходить до designated. Це нормальний ланцюжок.
Компілювальний приклад: деморежим без копіпасти
import Foundation
extension LibraryApp {
convenience init(withSampleData: Bool) {
if withSampleData {
let demo = [Book(title: "1984"), Book(title: "Dune")]
self.init(name: "Демо-бібліотека", books: demo, isDebug: true)
} else {
self.init()
}
}
}
let demoApp = LibraryApp(withSampleData: true)
print(demoApp.books.count) // 2
У цьому прикладі видно важливе: convenience init може вибирати різні шляхи, але на кожному шляху він має викликати self.init(...).
Додаткове налаштування після self.init(...)
Іноді convenience init робить не лише «підстановку параметрів», а й невелике додаткове налаштування після того, як обʼєкт уже зібрано. Це дозволено: після self.init(...) обʼєкт вважається повністю ініціалізованим, і ви можете звертатися до self, змінювати var-властивості та викликати методи. Але робити це треба обережно й без важкої логіки.
Головне — не перетворювати convenience init на «другий центр управління польотами». Він і далі має лишатися легким і передбачуваним: трохи нормалізувати дані, трохи налаштувати режим, трохи підготувати значення.
Компілювальний приклад: вмикаємо debug після делегування
import Foundation
extension LibraryApp {
convenience init(debug: Bool) {
self.init()
self.isDebug = debug
if debug {
print("Режим налагодження увімкнено") // Режим налагодження увімкнено
}
}
}
let dbg = LibraryApp(debug: true)
print(dbg.isDebug) // true
Тут тонкий момент: ми делегували в self.init() — ланцюжок дійшов до designated-ініціалізатора, — і лише потім змінюємо isDebug. До self.init() це було б заборонено.
Міні-рефакторинг: як convenience init прибирає копіпасту
Дуже типова еволюція коду виглядає так. Спочатку ви пишете один init. Потім вам потрібен «ще один, простіший» — ви копіюєте присвоєння. Потім потрібен «ще один» — і ви копіюєте знову. У якийсь момент ви змінюєте одне поле, і половина ініціалізаторів стає застарілою. Це той самий момент, коли convenience init перетворюється з «теорії мови» на економію ваших нервових клітин.
Уявімо, що ви помилково зробили два designated-ініціалізатори, кожен із яких вручну призначає властивості. Це працює, але відверто пахне копіпастою.
Схема: погано — дублювання логіки
init(name: String, books: [Book], isDebug: Bool) { ... }
init(name: String) {
self.name = name
self.books = []
self.isDebug = false
}
Якщо завтра ви вирішите, що isDebug за замовчуванням має бути true у dev-збірці, вам доведеться памʼятати про обидва місця.
Правильний рефакторинг такий: залишаємо один designated init, а спрощений робимо convenience.
Компілювальний приклад: добре — одне джерело правил
import Foundation
class FixedLibraryApp {
let name: String
var books: [Book]
var isDebug: Bool
init(name: String, books: [Book], isDebug: Bool) {
self.name = name
self.books = books
self.isDebug = isDebug
}
convenience init(name: String) {
self.init(name: name, books: [], isDebug: false)
}
}
Тепер усі «правила збирання обʼєкта» живуть у designated init, а convenience — просто зручний вхід.
3. convenience init і параметри за замовчуванням
У новачків часто виникає чесне запитання: «А навіщо convenience init, якщо можна зробити параметри за замовчуванням?» Запитання чудове, бо і те, і те розвʼязує схожу задачу: спростити створення обʼєкта. Але на рівні API та читабельності вони відчуваються по-різному.
Параметри за замовчуванням хороші, коли у вас один ініціалізатор, а відмінності між варіантами створення — це буквально «підставте інше число або рядок». convenience init хороший, коли ви хочете дати іменований сценарій створення, який читається як намір: «створи порожню бібліотеку», «створи демо-бібліотеку», «створи із сирого користувацького введення».
Компілювальний приклад: один designated init із дефолтами
import Foundation
class SimpleConfig {
let host: String
let port: Int
init(host: String = "localhost", port: Int = 8080) {
self.host = host
self.port = port
}
}
let a = SimpleConfig()
print(a.host, a.port) // localhost 8080
Поки сценаріїв небагато, це виглядає дуже охайно.
Компілювальний приклад: коли сценарій краще назвати
import Foundation
class ConnectionConfig {
let host: String
let port: Int
init(host: String, port: Int) {
self.host = host
self.port = port
}
convenience init(localhost: Bool) {
self.init(host: localhost ? "localhost" : "example.com", port: 8080)
}
}
let local = ConnectionConfig(localhost: true)
print(local.host) // localhost
Тут localhost: true може виглядати дивно, і це зроблено навмисно, щоб показати проблему: краще мати іменований вхід на кшталт init.local() або фабрику. Але фабрики — це вже розмова про методи типу, а ми сьогодні тримаємо фокус саме на convenience init. Головне — ви побачили думку: convenience дозволяє виразити сенс сценарію.
4. Типові помилки під час роботи з convenience init
Помилка №1: копіювати присвоєння замість делегування.
Новачки часто пишуть ще один init і знову вручну призначають self.name, self.books, self.isDebug. Код компілюється, але ви втрачаєте головну перевагу: одне джерело правди про те, як обʼєкт має виглядати на старті. Після кількох змін моделі такі init починають розходитись і породжують баги, які складно повʼязати з причиною.
Помилка №2: намагатися чіпати self до self.init(...).
Найпоширеніша помилка компілятора: усередині convenience init ви захотіли зробити self.isDebug = true, а потім уже викликати self.init(...). Для Swift це логічно неправильно: ви намагаєтеся змінювати обʼєкт, який іще не зібраний. Правильний порядок — спочатку делегування, потім будь-які дії з self.
Помилка №3: випадкова безкінечна рекурсія ініціалізаторів.
Це виглядає невинно: convenience init() { self.init() }. Компілятор не завжди може «за змістом» зрозуміти, що ви зробили кільце, і в результаті ви отримаєте падіння під час виконання через переповнення стека. Корисна звичка — подумки малювати стрілочки делегування й перевіряти, що ланцюжок завершується на designated init, а не ходить по колу.
Помилка №4: перетворювати convenience init на місце для важкої логіки.
Іноді хочеться: «раз уже я тут, давай одразу прочитаю файл, сходжу в мережу, розпарсю все, залогую, а заодно зварю каву». Але ініціалізатор має робити одну річ: приводити обʼєкт до коректного початкового стану. Якщо логіка обʼємна, краще винести її в окремий метод або окремий шар. Інакше обʼєкт стає важко створювати, важко тестувати й важко розуміти.
Помилка №5: плодити десятки варіантів convenience init «про всяк випадок».
Кожен новий ініціалізатор — це частина публічного API вашого типу. Якщо варіантів занадто багато, користувачеві класу складно вибрати «правильний» спосіб створення, а вам — підтримувати таке різноманіття. Гарне правило смаку: залишайте лише ті convenience init, які справді виражають різні сценарії створення і помітно покращують читабельність коду.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ