1. Зачем нужна инициализация
Инициализация — это момент, когда объект класса «рождается» и должен сразу оказаться в валидном, пригодном к жизни состоянии. Можно думать об этом как о чек-листе пилота: прежде чем взлететь, все тумблеры должны быть в понятных позициях. Если какое-то stored-свойство не получило значение, объект фактически «недособран», и 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 properties, объект — как ноутбук без установленной операционной системы. Корпус есть, клавиатура есть, но запускать «Photoshop» рановато.
Отсюда вытекают практические правила:
- сначала присваиваем значения stored-свойствам;
- только потом вызываем методы, которые используют эти свойства;
- и аккуратно относимся к логике, которая пытается использовать self «слишком рано».
Пример: метод вызывается после инициализации свойств
class Greeter {
var name: String
init(name: String) {
self.name = name
print("Hello, \(self.name)!") // Hello, 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 разрастается, часто это сигнал: часть логики стоит вынести в отдельные методы (и вызывать их уже после создания объекта), либо пересмотреть, какие свойства действительно обязательны на старте.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ