Перейти к содержимому

Лекция 3. Инициализация, деинициализация и жизненный цикл объектов

Цели лекции

После занятия студент должен уметь:

  • объяснить требования Swift к полной инициализации;
  • создавать обычные и проваливающиеся инициализаторы;
  • различать designated и convenience initializer;
  • понимать правила делегирования инициализации в иерархии классов;
  • использовать required init и значения по умолчанию;
  • применять lazy осознанно;
  • объяснить роль ARC и момент вызова deinit;
  • безопасно освобождать принадлежащие объекту ресурсы.

1. Зачем инициализация выделена в отдельный механизм

Инициализация переводит новое значение или объект из состояния «ещё не существует» в корректное начальное состояние. Swift не позволяет получить доступ к частично созданному экземпляру.

struct User {
let id: Int
var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}

К завершению init каждое хранимое свойство без значения по умолчанию должно получить значение. Это проверяется компилятором.

final class BrokenUser {
let id: Int
let name: String
init(id: Int) {
self.id = id
// Ошибка: name не инициализирован.
}
}

Инициализатор не является обычным методом и не пишется с func. Он не возвращает экземпляр явно: успешное завершение означает, что экземпляр готов.


2. Значения по умолчанию и автоматические инициализаторы

Свойство можно инициализировать прямо в объявлении:

struct Settings {
var theme = "system"
var notificationsEnabled = true
}
let settings = Settings()

Структура без пользовательского init получает memberwise initializer. Класс может получить пустой init() только тогда, когда все свойства имеют значения по умолчанию и нет препятствующей иерархии наследования.

final class SessionState {
var token: String? = nil
var retryCount = 0
}
let session = SessionState()

Значение по умолчанию подходит для действительно естественного состояния. Не следует подставлять пустую строку или ноль только ради обхода требований компилятора, если такое состояние некорректно для модели.


3. Несколько инициализаторов структуры

Тип может предлагать несколько способов создания:

struct Temperature {
var celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
init(fahrenheit: Double) {
self.init(celsius: (fahrenheit - 32) * 5 / 9)
}
}

self.init делегирует работу другому инициализатору того же значимого типа. Это снижает дублирование и оставляет единый путь настройки состояния.

Для сохранения автоматически созданного memberwise initializer пользовательский инициализатор структуры можно вынести в extension:

struct Size {
var width: Double
var height: Double
}
extension Size {
init(square side: Double) {
self.init(width: side, height: side)
}
}
let rectangle = Size(width: 10, height: 20)
let square = Size(square: 10)

4. Проваливающийся инициализатор

Если входные данные могут не позволить создать корректный экземпляр, используйте init?.

struct EmailAddress: Equatable {
let value: String
init?(_ rawValue: String) {
let parts = rawValue.split(separator: "@", omittingEmptySubsequences: false)
guard parts.count == 2,
!parts[0].isEmpty,
!parts[1].isEmpty else {
return nil
}
self.value = rawValue
}
}
let valid = EmailAddress("student@example.com")
let invalid = EmailAddress("wrong") // nil

Преимущество init?: некорректный экземпляр не появляется. Цена — вызывающий код должен обработать optional.

guard let email = EmailAddress(input) else {
print("Некорректный адрес")
return
}

Если причина ошибки важна, вместо init? часто лучше использовать фабричный метод с throws. Обработку ошибок мы подробно изучим позже.


5. Designated и convenience initializer

У классов инициализаторы делятся на два вида.

Designated initializer полностью инициализирует свойства своего класса и передаёт инициализацию вверх по иерархии.

Convenience initializer предоставляет дополнительный удобный способ создания и обязан делегировать другому инициализатору того же класса.

final class Document {
let title: String
let body: String
init(title: String, body: String) {
self.title = title
self.body = body
}
convenience init(title: String) {
self.init(title: title, body: "")
}
convenience init() {
self.init(title: "Без названия")
}
}

Цепочка convenience initializer в итоге должна прийти к designated initializer. Это правило гарантирует, что полная инициализация сосредоточена в ограниченном числе мест.


6. Делегирование в иерархии классов

Рассмотрим базовый и производный класс:

class Vehicle {
let identifier: String
private(set) var mileage: Int
init(identifier: String, mileage: Int = 0) {
self.identifier = identifier
self.mileage = mileage
}
}
final class ElectricCar: Vehicle {
let batteryCapacity: Int
init(identifier: String, mileage: Int = 0, batteryCapacity: Int) {
self.batteryCapacity = batteryCapacity
super.init(identifier: identifier, mileage: mileage)
}
}

Designated initializer подкласса сначала инициализирует все свойства, объявленные самим подклассом, а затем вызывает designated initializer суперкласса.

Правила делегирования удобно запомнить так:

  • designated initializer делегирует вверх через super.init;
  • convenience initializer делегирует вбок через self.init;
  • convenience initializer не вызывает super.init напрямую.

7. Двухфазная инициализация классов

Swift инициализирует класс в две фазы.

Фаза 1: создание корректного хранилища

  1. Инициализатор подкласса присваивает его собственные свойства.
  2. Вызывается super.init.
  3. Процесс продолжается вверх до корневого класса.
  4. После инициализации всех свойств память объекта считается корректной.

Фаза 2: настройка

Инициализаторы получают доступ к self, могут изменять свойства и вызывать методы. Благодаря этому переопределённый метод не вызывается на частично инициализированном объекте.

Практическое следствие: до super.init подкласс не может свободно использовать self, а после вызова уже может выполнять дополнительную настройку.


8. Наследование инициализаторов

Инициализаторы классов не наследуются автоматически во всех случаях. Swift применяет специальные правила, чтобы подкласс случайно не получил способ создания, который не инициализирует его новые свойства.

Упрощённо:

  • если подкласс даёт значения по умолчанию всем новым свойствам и не объявляет designated initializer, он может унаследовать designated initializer родителя;
  • если подкласс реализует или наследует все designated initializer родителя, он наследует convenience initializer родителя.

В учебном коде безопаснее явно определить нужные способы создания и не полагаться на неочевидное наследование.


9. required init

required требует, чтобы каждый подкласс предоставлял данный инициализатор.

class Message {
let text: String
required init(text: String) {
self.text = text
}
}
final class ImportantMessage: Message {
let priority: Int
required init(text: String) {
self.priority = 1
super.init(text: text)
}
}

В реализации подкласса повторно писать override рядом с required не нужно. Такой контракт полезен, когда код создаёт экземпляры разных подклассов через метатип.


10. Замыкания как значения свойств по умолчанию

Сложное начальное значение можно вычислить замыканием, которое выполняется один раз при создании экземпляра:

struct Board {
let cells: [[Int]] = {
Array(repeating: Array(repeating: 0, count: 8), count: 8)
}()
}

Во время выполнения этого замыкания остальная часть экземпляра ещё не инициализирована, поэтому обращаться к self нельзя.


11. Ленивые свойства

lazy откладывает вычисление свойства до первого обращения.

final class Report {
let rows: [Int]
lazy var formattedText: String = {
rows.map(String.init).joined(separator: ", ")
}()
init(rows: [Int]) {
self.rows = rows
}
}

Ленивое свойство всегда объявляется через var, потому что его значение присваивается после завершения инициализации.

lazy полезен, когда:

  • вычисление дорогое и результат может не понадобиться;
  • для создания свойства нужен полностью инициализированный self;
  • зависимость хочется создать по требованию.

Важно: обычное lazy не гарантирует потокобезопасную однократную инициализацию при одновременном доступе из нескольких потоков.


12. ARC и время жизни класса

Экземпляры классов управляются механизмом Automatic Reference Counting. ARC учитывает сильные ссылки на объект. Когда сильных ссылок не остаётся, объект освобождается.

final class DebugObject {
let name: String
init(name: String) {
self.name = name
print("Создан: \(name)")
}
deinit {
print("Удалён: \(name)")
}
}
do {
let object = DebugObject(name: "temporary")
print(object.name)
} // после выхода из области сильная ссылка исчезает

ARC не является сборщиком мусора с периодическими паузами. Подсчёт ссылок обновляется по мере выполнения программы. Однако компилятор вправе завершить время жизни локальной переменной после её последнего использования, поэтому не следует строить бизнес-логику на точной строке вызова deinit.


13. Деинициализатор

deinit доступен только классам. Он не принимает параметры и не вызывается вручную.

final class ResourceLease {
let resourceName: String
init(resourceName: String) {
self.resourceName = resourceName
print("Ресурс получен: \(resourceName)")
}
deinit {
print("Ресурс освобождён: \(resourceName)")
}
}

В deinit освобождают ресурсы, принадлежащие объекту: закрывают дескриптор файла, отменяют регистрацию наблюдателя или возвращают ограниченный ресурс.

Предпочтительно иметь явный метод close()/cancel() для своевременного освобождения, а deinit использовать как страховку. Это делает момент операции видимым и позволяет обработать возможную ошибку.


14. Идемпотентное освобождение ресурса

Метод освобождения должен безопасно переносить повторный вызов.

final class Connection {
private(set) var isOpen = true
func close() {
guard isOpen else { return }
isOpen = false
print("Соединение закрыто")
}
deinit {
close()
}
}

Если клиент уже вызвал close(), деинициализатор ничего не делает. Если клиент забыл — объект всё равно выполняет резервную очистку.

Не следует позволять ссылке на self покинуть deinit или запускать из него долгую асинхронную работу. Объект уже завершает жизненный цикл.


15. Типичные ошибки

Некорректные значения по умолчанию

Пустой id ради автоматического init() создаёт объект, который затем приходится долго «донастраивать». Лучше потребовать обязательные данные в инициализаторе.

Дублирование в нескольких init

Удобные способы создания должны делегировать основному пути, а не копировать проверки и присваивания.

Использование self слишком рано

В иерархии классов нельзя вызывать методы и читать унаследованное состояние до завершения первой фазы инициализации.

Надежда только на deinit

Внешний ресурс лучше освобождать явно. deinit не подходит для операции, чью ошибку нужно показать пользователю или повторить.

lazy для сокрытия неправильной архитектуры

Ленивая инициализация полезна для стоимости и порядка создания, но не должна маскировать циклические зависимости между объектами.


16. Вопросы для самопроверки

  1. Когда экземпляр считается полностью инициализированным?
  2. Чем init? отличается от обычного init?
  3. Что такое designated initializer?
  4. Куда обязан делегировать convenience initializer?
  5. В каком порядке подкласс инициализирует свои свойства и вызывает super.init?
  6. Зачем нужна двухфазная инициализация?
  7. Что гарантирует required init?
  8. Почему lazy объявляется только через var?
  9. Когда ARC освобождает экземпляр класса?
  10. Почему явный close() часто лучше одной очистки в deinit?

Итоги

  • Swift не допускает частично инициализированных экземпляров.
  • init? предотвращает создание модели с нарушенными инвариантами.
  • Designated initializer создаёт полное состояние; convenience initializer делегирует основному пути.
  • Инициализация иерархии классов выполняется в две фазы.
  • lazy откладывает вычисление до первого доступа.
  • ARC освобождает класс после исчезновения сильных ссылок, а deinit завершает жизненный цикл и страхует освобождение ресурсов.