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 ? "Anonymous" : trimmed
        self.init(name: finalName)
    }
}

let a = UserProfile(rawName: "   ")
print(a.name) // Anonymous

Здесь 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: "Home Library")
print(app.books.count) // 0

Здесь convenience init(name:) — это просто «короткая форма»: имя задано, остальное — дефолтами.

Компилируемый пример: convenience init() для быстрого старта

import Foundation

extension LibraryApp {
    convenience init() {
        self.init(name: "My Library")
    }
}

let quick = LibraryApp()
print(quick.name) // My Library

Обратите внимание: 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: "Demo Library", 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("Debug mode is ON") // Debug mode is ON
        }
    }
}

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

1
Задача
Swift SELF, 36 уровень, 2 лекция
Недоступна
Чистое имя
Чистое имя
1
Задача
Swift SELF, 36 уровень, 2 лекция
Недоступна
Настройка сервера
Настройка сервера
1
Задача
Swift SELF, 36 уровень, 2 лекция
Недоступна
Демо корзина
Демо корзина
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ