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, которые реально выражают разные сценарии создания и заметно улучшают читаемость кода.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ