JavaRush /Курсы /Swift SELF /init: базовые правила инициализации классов

init: базовые правила инициализации классов

Swift SELF
36 уровень , 1 лекция
Открыта

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% случаев значение именно такое».

Ниже — таблица, которая помогает выбрать подход.

Подход Как выглядит Когда подходит Что может пойти не так
Значение по умолчанию
var isActive = true
«разумный дефолт» очевиден можно случайно забыть установить «настоящее» значение
Присваивание в init
init(isActive: Bool) { ... }
значение обязательно для корректного состояния нужно писать больше кода, но это честная цена

Мини-пример: часть свойств с дефолтом, часть — обязательные

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
    }
}

Обратите внимание на композицию решений:

sessionIDlet, потому что это «номер сессии», он не должен меняться.
startedAt — тоже let: стартовали один раз.
handledCommandsvar: число команд растёт.

Добавим маленькое поведение и проверим, что объект живой

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 разрастается, часто это сигнал: часть логики стоит вынести в отдельные методы (и вызывать их уже после создания объекта), либо пересмотреть, какие свойства действительно обязательны на старте.

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