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

init: базові правила ініціалізації класів

Swift SELF
Рівень 36 , Лекція 1
Відкрита

1. Навіщо потрібна ініціалізація

Ініціалізація — це момент, коли обʼєкт класу «народжується» і має одразу опинитися в коректному, придатному до роботи стані. Можна уявити це як чек-лист пілота: перш ніж злітати, усі тумблери мають бути у зрозумілих позиціях. Якщо якась властивість, що зберігається не отримала значення, обʼєкт фактично «недозібраний», і 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-властивостям, обʼєкт — як ноутбук без установленої операційної системи. Корпус є, клавіатура є, але запускати «Photoshop» зарано.

Звідси випливають практичні правила:

  • спочатку присвоюємо значення stored-властивостям;
  • лише потім викликаємо методи, які використовують ці властивості;
  • і обережно ставимося до логіки, яка намагається використати self «занадто рано».

Приклад: метод викликається після ініціалізації властивостей

class Greeter {
    var name: String

    init(name: String) {
        self.name = name
        print("Привіт, \(self.name)!") // Привіт, 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 розростається, часто це сигнал: частину логіки варто винести в окремі методи (і викликати їх уже після створення обʼєкта) або переглянути, які властивості справді обовʼязкові на старті.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ