JavaRush /Курси /Swift SELF /deinit — коли він спрацьовує і навіщо потрібен

deinit — коли він спрацьовує і навіщо потрібен

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

1. Чому існує deinit

Коли ви тільки починаєте писати застосунки, може здаватися, що обʼєкти безсмертні: створили User(), викликали print — і світ прекрасний. Але в реальних застосунках обʼєкти живуть не у вакуумі: вони можуть тримати ресурси, підписуватися на події, накопичувати статистику, вмикати режим налагодження. Усе це бажано акуратно завершувати, коли обʼєкт уже не потрібен.

І ось тут зʼявляється deinit — деініціалізатор класу. Його можна уявити як «фінальну сцену» персонажа в серіалі: він не зобовʼязаний помирати драматично, але має хоча б тихо прибрати за собою чашку (або хоча б вивести в лог, що чашка таки була). При цьому deinit запускається автоматично, коли екземпляр стає недосяжним за посиланнями; на низькому рівні це повʼязано з тим, що в обʼєкта закінчуються власники, і в Swift це формулюють так: коли лічильник посилань досягає нуля, викликається deinit.

Важливо: на цьому етапі ми не розбираємо механіку — чому так і хто рахує посилання. Нам достатньо правильної моделі: обʼєкт живе, доки десь на нього зберігається посилання.

2. deinit у Swift: синтаксис і обмеження

Коли ви вперше бачите deinit, він може здаватися схожим на функцію або метод, але це особлива конструкція мови із суворими правилами. Це навіть зручно: менше свободи — менше шансів випадково влаштувати собі «прощальний феєрверк» у продакшені.

deinit буває лише у класів (та інших посилальних типів мови, але ми зараз говоримо про class). Він не приймає параметрів, його не можна викликати вручну, і він спрацьовує рівно один раз для конкретного екземпляра. Це не «метод очищення за кнопкою», а автоматичний механізм завершення життя обʼєкта.

Мініприклад — просто щоб побачити форму:

import Foundation

class Tracer {
    let name: String

    init(name: String) {
        self.name = name
        print("ініціалізація \(name)") // ініціалізація A
    }

    deinit {
        print("деініціалізація \(name)") // деініціалізація A
    }
}

Тут ми вже бачимо базову ідею: init — «народився», deinit — «пішов зі сцени». При цьому ми не вирішуємо, коли він піде: ми лише можемо зрозуміти, що це станеться, коли обʼєкт перестане бути недосяжним.

3. Коли спрацьовує deinit

Зараз корисно зробити паузу й чесно визнати: точний момент виклику deinit у реальному застосунку може залежати від оптимізацій і деталей виконання. Але для впевненого програмування новачкові достатньо робочої моделі: якщо на обʼєкт більше ніхто не посилається, deinit може виконатися.

Для звичайного коду це означає, що найчастіше deinit спрацьовує у двох типових ситуаціях. Перша — ви тримали посилання в опціоналі й присвоїли туди nil. Друга — посилання було локальною змінною всередині функції, і функція завершилася, тож змінна зникла.

Почнемо з найнаочнішого сценарію — опціонал і nil.

Обнуляємо опціонал → втрачаємо посилання → обʼєкт може деініціалізуватися

import Foundation

class Tracer {
    let name: String
    init(name: String) {
        self.name = name
        print("ініціалізація \(name)") // ініціалізація A
    }
    deinit {
        print("деініціалізація \(name)") // деініціалізація A
    }
}

var t: Tracer? = Tracer(name: "A")
t = nil

Чому це працює так наочно? Бо опціонал — це змінна, яка може зберігати або посилання на обʼєкт (.some(...)), або nil. Коли ви робите t = nil, ви прибираєте одне конкретне посилання. Якщо це було останнє посилання — обʼєкт стає недосяжним, і deinit виконується.

Виходимо з області видимості → локальне посилання зникає

import Foundation

class Tracer {
    let name: String
    init(name: String) { self.name = name; print("ініціалізація \(name)") }   // ініціалізація temp
    deinit { print("деініціалізація \(name)") }                                // деініціалізація temp
}

func demoScope() {
    let x = Tracer(name: "temp")
    print("усередині:", x.name) // усередині: temp
}

demoScope()
print("після demoScope")     // після demoScope

Інтуїтивна картинка така: поки виконується demoScope(), існує змінна x, а отже й посилання. Щойно функція завершується, x зникає. Якщо більше ніде посилання не зберігалося — обʼєкт стає недосяжним.

4. Чому deinit може не спрацювати одразу: аліасинг

Зазвичай перша несподіванка з deinit у новачків трапляється так: ви зробили t = nil, а deinit не викликався. Перше відчуття — «Swift зламався». Насправді майже завжди винне те, що ви вже знаєте з теми про посилальні типи: аліасинг.

Якщо на один обʼєкт посилаються дві змінні, то обнулення однієї змінної не знищує обʼєкт — друга змінна все ще тримає посилання.

import Foundation

class Tracer {
    let name: String
    init(name: String) { self.name = name; print("ініціалізація \(name)") }   // ініціалізація спільного
    deinit { print("деініціалізація \(name)") }                                // деініціалізація спільного
}

var a: Tracer? = Tracer(name: "shared")
var b: Tracer? = a

a = nil
print("a = nil, але b досі тримає обʼєкт") // a = nil, але b досі тримає обʼєкт

b = nil
print("тепер від a/b не лишилося посилань")             // тепер від a/b не лишилося посилань

Це чудовий «рентген» на аліасинг: deinit відбудеться лише після b = nil, бо тільки тоді зникне останнє посилання.

Можна навіть подумки намалювати таку схему:

flowchart LR
    A[a] --> O((Tracer))
    B[b] --> O
    A -. "a = nil" .-> X[посилання a зникло]
    B --> O
    B -. "b = nil" .-> Y[посилання b зникло]

Поки хоча б одна стрілка веде до обʼєкта, він продовжує жити.

5. Практичний сенс deinit

Часто хочеться думати про deinit як про місце, де ми «точно все почистимо». Але тут є тонкий філософський момент: deinit — чудове місце для фіналізації, але погане місце для важливої бізнес-логіки.

Причина проста: deinit спрацьовує тоді, коли обʼєкт став недосяжним. А це майже ніколи не збігається з вашим сценарієм користувача. Наприклад, якщо ви пишете CLI-застосунок, користувачеві важливо, щоб «збереження» відбулося коли він увів команду save, а не тоді, коли обʼєкт менеджера раптом зник із памʼяті.

Тому в прикладному сенсі deinit особливо корисний для двох речей. По-перше, для діагностики: перевірити, що обʼєкт справді звільняється. По-друге, для звільнення ресурсів у тих місцях, де це безпечно й не впливає на сенс роботи застосунку. Технічно це можуть бути закриття дескрипторів, видалення тимчасових даних, відписка від спостережень — але ми зараз не будемо заглиблюватися в платформенні деталі; нам важливий принцип: коротко, передбачувано, без критичних дій.

6. Приклад: сеанс бібліотеки та deinit

Щоб не вивчати deinit у вакуумі, продовжимо наш навчальний консольний застосунок у стилі «керування бібліотекою»: ми десь зберігаємо книги (поки це можуть бути просто структури й масиви), а для роботи створюємо обʼєкт «сеансу» — контекст поточного запуску та введення команд.

Ми спеціально зробимо так, щоб deinit друкував повідомлення. Це не «бізнес-фіча», а відладкова лампочка: якщо лампочка не гасне — значить, обʼєкт хтось тримає.

import Foundation

class LibrarySession {
    let userName: String

    init(userName: String) {
        self.userName = userName
        print("Сеанс розпочато для \(userName)") // Сеанс розпочато для Ana
    }

    deinit {
        print("Сеанс завершено для \(userName)")   // Сеанс завершено для Ana
    }
}

Тепер покажемо типову ситуацію: створили сеанс усередині функції, попрацювали, вийшли — сеанс завершився.

import Foundation

class LibrarySession {
    let userName: String
    init(userName: String) { self.userName = userName; print("старт \(userName)") } // старт Ana
    deinit { print("кінець \(userName)") }                                             // кінець Ana
}

func runOnce() {
    let session = LibrarySession(userName: "Ana")
    print("виконуємо роботу...") // виконуємо роботу...
}

runOnce()
print("готово")              // готово

Якщо все добре, ви побачите кінець Ana після виходу з runOnce(). Це означає, що після виконання функції не залишилося посилань на session.

Тепер зробимо навмисно «небезпечний» варіант: винесемо посилання назовні.

import Foundation

class LibrarySession {
    let userName: String
    init(userName: String) { self.userName = userName; print("старт \(userName)") }
    deinit { print("кінець \(userName)") }
}

var globalSession: LibrarySession? = nil

func startApp() {
    let s = LibrarySession(userName: "Sam")
    globalSession = s
    print("застосунок запущено") // застосунок запущено
}

startApp()
globalSession = nil

Тут deinit спрацює лише після globalSession = nil, бо глобальна змінна — це «довгоживуча рука», яка тримає обʼєкт. Це не помилка саме по собі, але чудовий спосіб побачити: час життя обʼєкта визначається тим, де ви зберігаєте посилання.

7. Корисні нюанси deinit

Що можна і чого не можна робити

З deinit легко впасти в крайнощі. Одні починають запихати туди взагалі все: «і збереження, і надсилання статистики, і друк звіту, і ще каву зварити». Інші, навпаки, бояться його як темряви в дитячій кімнаті: «раптом він спрацює в неочікуваний момент».

Правильний баланс такий: deinit має бути коротким, не породжувати винятків (так, deinit не призначений для того, щоб «повідомляти світові про помилки»), і не залежати від зовнішнього порядку подій. Намагайтеся думати так: якщо deinit виконається на мілісекунду раніше або пізніше, застосунок не повинен змінювати сенс.

Для навчання дуже корисно використовувати deinit як «датчик» і друкувати туди лог. Це допомагає сформувати правильну інтуїцію: обʼєкт існує, доки до нього можна дістатися, а щойно перестав — деініціалізується. У специфікації Swift окремо підкреслюють, що deinit запускається, коли обʼєкт перестав мати власників (на рівні реалізації це описують як досягнення нуля за лічильником посилань).

Табличка-інтуїція: що впливає на deinit

Іноді корисніше один раз побачити загальну карту, ніж десять разів перечитувати одне й те саме пояснення. Нижче — проста таблиця: що зазвичай подовжує життя обʼєкта і що зазвичай його завершує.

Ситуація в коді Що відбувається з посиланням Що це означає для deinit
Локальна змінна всередині функції (let x = Obj()) Посилання живе до кінця функції deinit зазвичай після виходу з функції
Присвоєння b = a (аліасинг) Зʼявилося ще одне посилання deinit буде пізніше, доки не зникнуть обидва
Опціонал var x: Obj? і x = nil Посилання видалено deinit можливий, якщо це було останнє посилання
Посилання збережене «назовні» (глобально/у полі іншого обʼєкта) Посилання стає довгоживучим deinit не відбудеться, доки ви не приберете це посилання

Зверніть увагу: таблиця спеціально говорить «зазвичай». Вона не про магію, а про практичну логіку: де зберігається посилання — там і визначається час життя.

8. Типові помилки під час роботи з deinit

Помилка № 1: очікувати, що deinit можна викликати вручну.
Іноді хочеться написати щось на кшталт obj.deinit() за аналогією зі звичайними методами. Але deinit — не метод «за кнопкою», він викликається автоматично один раз за життєвий цикл екземпляра. Якщо вам потрібно «вручну закривати» ресурс — робіть звичайний метод (close, stop, finish) і викликайте його явно, а deinit залишайте як страховку та місце для фінальної діагностики.

Помилка № 2: думати, що x = nil гарантовано знищить обʼєкт.
nil прибирає посилання лише з конкретної змінної. Якщо десь є ще аліас (наприклад, ви раніше зробили b = a або зберегли обʼєкт в іншому місці), то обʼєкт продовжить жити. Це не баг, а прямий наслідок посилальної природи class: deinit станеться лише тоді, коли зникне останнє посилання.

Помилка № 3: ховати в deinit важливу бізнес-логіку.
Наприклад, «у deinit автоматично зберігаємо дані». Це виглядає зручно, але вкрай крихко: момент деініціалізації залежить від того, коли обʼєкт став недосяжним, а це не обовʼязково збігається з моментом, коли користувачеві справді потрібно зберегти результат. Хороший тон — робити важливі дії явно (методом, командою, подією), а deinit використовувати для безпечного завершення та налагодження.

Помилка № 4: розраховувати на deinit як на «подію кінця застосунку».
deinit стосується конкретного екземпляра, а не файла, функції чи всього застосунку. В одному й тому самому застосунку один обʼєкт може деініціалізуватися за секунду після створення, інший — за хвилину, третій — узагалі лише наприкінці виконання. Якщо ви використовуєте deinit як сигнал «застосунок завершується», логіка стає непередбачуваною.

Помилка № 5: писати в deinit забагато коду і складних залежностей.
Чим складніший deinit, тим вищий шанс, що він стане джерелом дивних ефектів: неочікуваний вивід, зайва робота, побічні дії. Правильний deinit зазвичай короткий: відпустити ресурси, вивести діагностичне повідомлення, привести обʼєкт до «тиші» без зовнішніх залежностей.

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