JavaRush /Курси /Swift SELF /convenience init — навіщо він потрібен і як працює

convenience init — навіщо він потрібен і як працює

Swift SELF
Рівень 36 , Лекція 2
Відкрита

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, які справді виражають різні сценарії створення і помітно покращують читабельність коду.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ