JavaRush /Курси /Swift SELF /ARC і сильні посилання: що таке reference count

ARC і сильні посилання: що таке reference count

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

1. Навіщо в Swift потрібен ARC для class

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

Для struct і enum — типів-значень — історія одна, а для class — інша. Сьогодні говоримо лише про class, тому що ARC (Automatic Reference Counting) — це механізм, який керує часом життя обʼєктів класів.

ARC можна уявити як досить педантичного бухгалтера: він не питає, чи потрібен вам обʼєкт, а дивиться на сильні посилання. Поки є хоча б одне, обʼєкт живе. Коли останнє зникає — обʼєкт знищується, і викликається deinit.

2. Strong reference: що це означає в коді

Слово «strong» часто лякає новачків, бо звучить як «сильна магія». На практиці все простіше: strong reference — це звичайне посилання на обʼєкт класу, яке ви створюєте, коли пишете let або var і записуєте туди екземпляр класу.

Створімо маленький «трасувальник» і подивімося на життя обʼєкта очима консолі.


import Foundation

final class Tracer {
    let name: String

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

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

Тут Tracer не робить нічого корисного, як і багато класів на початкових етапах проєкту, зате чудово показує, коли він створюється і коли знищується.

Тепер найважливіша деталь: сильне посилання виникає в момент, коли ви присвоюєте обʼєкт змінній.

func demoStrongReference() {
    let t = Tracer("A")
    print("використання \(t.name)")
}

demoStrongReference()
print("після demoStrongReference")

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

3. Reference count як модель мислення

Час познайомитися з терміном reference count — це кількість сильних посилань на обʼєкт. Важливо одразу домовитися про спосіб мислення: reference count — це не те, що ви зобовʼязані «прочитати» як число у Swift. У звичайному коді ви цього не робите. Це модель, яка допомагає міркувати: «чому deinit уже викликався?» або «чому ще не викликався?».

Правило, яке варто запамʼятати майже як таблицю множення:

deinit викликається тоді, коли reference count обʼєкта досягає нуля.

Тобто ARC стежить за сильними посиланнями. Кожне з них ніби каже: «я відповідаю за те, щоб обʼєкт залишався живим». Коли таких «відповідальних» не лишається, обʼєкт видаляють.

Ось як це виглядає на мінітаймлайні:

t створено → refCount = 1 (змінна t утримує обʼєкт)
вихід за межі області видимості → refCount = 0
refCount став 0 → викликається deinit → памʼять звільняється

Це дуже проста модель. Вона не пояснює всіх тонкощів оптимізацій, але для практики — ідеальна.

4. Як strong-посилання зникають: scope, перезапис і nil

Сильні посилання у Swift зникають не тому, що ви більше не користуєтеся обʼєктом, а тому, що конкретна змінна або властивість перестає на нього посилатися. Є кілька типових сценаріїв, і кожен із них легко побачити наочно.

Вихід за межі області видимості

Коли локальна змінна перестає існувати — тобто ви виходите з функції або блоку — посилання зникає.

func demoScope() {
    let x = Tracer("scope")
    print("усередині demoScope")
}

demoScope()
// ініціалізація scope
// усередині demoScope
// деініціалізація scope

Якщо deinit у виведенні не зʼявився, це не означає, що Swift «забув». Це означає, що десь залишилося ще одне сильне посилання, і обʼєкт досі живий.

Перезапис змінної новим обʼєктом

Якщо змінна var вказує на обʼєкт і ви присвоюєте їй інший обʼєкт, старе посилання зникає.

func demoOverwrite() {
    var t = Tracer("first")
    t = Tracer("second")      // посилання на "first" зникає
    print("тепер містить second")
}

demoOverwrite()

Тут є цікавий момент: коли саме звільниться "first", залежить від того, чи залишилися на нього ще посилання. Якщо ні, то deinit для first ви побачите одразу після перезапису.

Явне зняття посилання через nil

Дуже зручний трюк для навчання, а іноді й для реальної логіки, — тримати посилання як Optional і явно обнуляти його.

func demoNil() {
    var t: Tracer? = Tracer("optional")
    print("створено")
    t = nil                  // прибрали сильне посилання
    print("звільнено")
}

demoNil()

Чому це корисно під час навчання? Бо ви буквально пишете в коді: «ось тут я перестаю володіти обʼєктом». А ARC відповідає: «прийнято».

5. Дві змінні — один обʼєкт: aliasing і deinit

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

func demoTwoStrongRefs() {
    var a: Tracer? = Tracer("shared")
    var b: Tracer? = a        // b тепер теж утримує той самий обʼєкт

    a = nil
    print("a = nil")

    b = nil
    print("b = nil")
}

demoTwoStrongRefs()

У цьому прикладі обʼєкт "shared" не може знищитися після a = nil, тому що b усе ще утримує його. І саме тут видно, як працює reference count: коли ви пишете var b: Tracer? = a, кількість сильних посилань зростає. Коли ви обнуляєте a, лічильник зменшується, але не до нуля. Лише після b = nil він доходить до нуля, і deinit нарешті має право відбутися.

6. Strong-посилання у властивостях і колекціях

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

Поки що не заглиблюймося у складні сценарії, але вже зараз важливо зафіксувати: сильним посиланням може бути не лише змінна, а й stored property та елемент колекції.

Stored property як власник

Зробімо мінізаготовку з нашого навчального CLI-застосунку. Уявімо, що ми будуємо консольну «бібліотеку», де є сесія роботи користувача.

final class AppSession {
    let tracer = Tracer("session")
}

func demoPropertyOwnership() {
    var session: AppSession? = AppSession()
    print("сесію створено")
    session = nil
    print("сесію звільнено")
}

demoPropertyOwnership()

Тут AppSession зберігає tracer у властивості. Поки живе session, живе й tracer. Коли session = nil, зникає сильне посилання на AppSession, він звільняється, а разом із ним звільняється і tracer. Це виглядає як «ланцюжок доміно», і загалом так і є: власники падають — обʼєкти, що їм належать, падають слідом.

Колекція утримує елементи

Колекції (Array, Dictionary, Set) утримують свої елементи. Якщо елемент — обʼєкт класу, то колекція тримає сильне посилання на цей обʼєкт.

func demoArrayHoldsObjects() {
    var items: [Tracer] = []
    items.append(Tracer("in array"))

    print("кількість:", items.count)
    items.removeAll() // видалили сильні посилання з масиву
    print("очищено")
}

demoArrayHoldsObjects()

Якщо ви обнулили власну змінну, але залишили обʼєкт у масиві, він і далі живий. І це не помилка. Це чесна логіка: масив сказав ARC «я власник».

7. Граф посилань: як мислити в дусі ARC

Коли проєкт виростає за межі кількох функцій, тримати все в голові лише як «лічильник» уже важко. Тому корисно додати ще одну модель: граф посилань. Вона не скасовує reference count, а просто робить міркування візуальними.

Уявіть, що в нас є обʼєкт LibraryApp, і він зберігає поточну сесію, а сесія зберігає трасер:

flowchart TD
    A[Бібліотечний застосунок] -->|сильне| S[Сесія застосунку]
    S -->|сильне| T[Трасер]

Поки існує LibraryApp і він зберігає AppSession, AppSession живий. Поки AppSession живий і зберігає Tracer, Tracer живий. ARC, по суті, робить просту річ: видаляє вузол, коли до нього не веде жодна стрілка сильного посилання.

І тут формується дуже корисна звичка: у будь-який момент часу вміти сказати, хто є «власником» обʼєкта. Не хто ним користується, а хто утримує його сильним посиланням.

8. Приклад із CLI: життєвий цикл команди

Щоб ця лекція не зводилася лише до абстрактних Tracer("A"), давайте зробимо приклад, схожий на реальний шматок консольного застосунку. Уявімо, що в нас є обʼєкт-команда, який створюється на одне введення користувача, а потім має зникнути.

final class Command {
    let raw: String

    init(raw: String) {
        self.raw = raw
        print("ініціалізація Command:", raw)
    }

    deinit {
        print("деініціалізація Command:", raw)
    }
}

Тепер змоделюймо одну ітерацію обробки:

func handleLine(_ line: String) {
    let cmd = Command(raw: line)
    print("обробка:", cmd.raw)
}

handleLine("help")
print("готово до наступного рядка")

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

Тепер, лише для наочності, покажімо, як можна випадково подовжити життя команди, якщо покласти її до сховища.

final class CommandHistory {
    var items: [Command] = []
}

func demoHistoryKeepsCommands() {
    let history = CommandHistory()
    history.items.append(Command(raw: "help"))
    history.items.append(Command(raw: "list"))
    print("збережено в історії:", history.items.count)
}

demoHistoryKeepsCommands()

Тут команди живуть доти, доки живий history і доки масив не очищено. Це нормально, якщо ви справді хочете історію. Але якщо ви не хотіли, а «воно саме збереглося», — це вже привід замислитися, де ви залишили сильне посилання.

Що важливо запамʼятати вже зараз

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

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

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

9. Типові помилки під час роботи з ARC і strong references

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

Помилка №2: намагатися «викликати deinit вручну».
Після досвіду зі звичайними методами рука іноді тягнеться написати щось на кшталт obj.deinit(). Так робити не можна: deinit викликається автоматично і рівно один раз, коли reference count стає нулем. Якщо вам потрібен ручний контроль ресурсів, це вирішується дизайном API, а не ручним викликом deinit.

Помилка №3: не розуміти, що b = a для класів — це не копія обʼєкта.
Коли ви пишете var b = a, ви не отримуєте «другий такий самий обʼєкт». Ви отримуєте друге посилання на той самий обʼєкт. У голові має спрацювати просте правило: «ага, reference count зріс, отже обʼєкт тепер живе довше». Це особливо помітно, якщо в обʼєкті є логування deinit, і воно «не спрацьовує», доки ви не обнулите обидві змінні.

Помилка №4: забувати про колекції як про власників.
Масив — це не «просто список». Це обʼєкт, який утримує свої елементи. Якщо елемент — клас, то колекція утримує сильне посилання. Тому сценарій «я обнулив змінну, але обʼєкт не помер» часто означає «він досі лежить у масиві».

Помилка №5: плутати Optional і «слабке посилання».
Optional сам по собі не робить посилання слабким. Tracer? — це все ще сильне посилання, просто його можна обнулити через nil. Це зручно для експериментів і явного керування володінням, але не змінює модель володіння: доки Optional містить обʼєкт — він утримується сильним посиланням.

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