JavaRush /Курси /Swift SELF /weak і unowned: відмінності та типові помилки

weak і unowned: відмінності та типові помилки

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

1. Чому взагалі є два слова: weak і unowned

Коли ви вперше бачите weak і unowned, хочеться запитати: «Навіщо два способи не утримувати об’єкт? Хіба не можна було лишити один?» Запитання справедливе. Але на практиці ці два ключові слова покривають два різні життєві сценарії: «посилання може стати порожнім — і це нормально» та «посилання не повинно ставати порожнім ніколи — це частина контракту». Саме тому мова й дає два інструменти.

ARC у Swift загалом влаштований так, щоб керування пам’яттю було передбачуваним і точним, на відміну від підходів зі збирачем сміття. Саме тому Swift цінують за продуктивність і контроль над ресурсами.

Водночас важливо пам’ятати: хоч ARC і знімає потребу вручну викликати retain/release, витоки — і просто неочікувані «об’єкти, які не зникають» — усе одно можливі. Найчастіше вони виникають через цикли утримання між об’єктами або через зв’язку «об’єкт ↔ замикання». Сьогодні розберемо лише одну частину цієї історії: як правильно розривати цикли між об’єктами, обираючи weak або unowned.

2. weak: посилання, яке вміє зникати

Якщо описувати weak по-людськи, це «посилання-спостерігач». Воно каже: «Мені цікаво знати про цей об’єкт, але я не збираюся тримати його живим». І є важлива деталь: якщо об’єкт зникне, weak-посилання автоматично стане nil. Звідси й ключова особливість, яка новачків спершу дратує, а потім рятує: weak завжди є Optional.

Саме тому weak — головний кандидат для випадків, коли зв’язок логічно може обірватися. Наприклад, квартира може тимчасово залишитися без мешканця, екран застосунку — без координатора, а в дочірнього об’єкта може зникнути власник. У таких сценаріях Swift змушує вас чесно обробляти відсутність значення, бо вона справді можлива.

Синтаксис weak і його наслідки

Тут усе доволі суворо:

  • weak можна застосовувати лише до посилальних типів (class), тому що сенс «не утримувати об’єкт» стосується саме об’єктів у купі.
  • weak-властивість завжди має тип Optional.
  • weak-властивість не можна оголосити як let, тому що вона може стати nil будь-якої миті, коли об’єкт звільниться. Отже, лише var.

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

Мініприклад: розриваємо цикл між об’єктами через weak

Нижче — приклад, який можна запустити як окремий файл. Він показує типовий патерн: один об’єкт володіє іншим (strong), а зворотне посилання — weak.

import Foundation

func demoWeakBreaksCycle() {
    final class Person {
        let name: String
        var apartment: Apartment?
        init(name: String) { self.name = name; print("ініціалізація Person \(name)") }
        deinit { print("деініціалізація Person \(name)") }
    }

    final class Apartment {
        let number: Int
        weak var tenant: Person?           // <-- не утримуємо мешканця
        init(number: Int) { self.number = number; print("ініціалізація Apartment \(number)") }
        deinit { print("деініціалізація Apartment \(number)") }
    }

    var p: Person? = Person(name: "Ann")
    var a: Apartment? = Apartment(number: 10)

    p?.apartment = a
    a?.tenant = p

    p = nil
    a = nil
}

demoWeakBreaksCycle()
// Можливий вивід (порядок deinit може відрізнятися):
// ініціалізація Person Ann
// ініціалізація Apartment 10
// деініціалізація Person Ann
// деініціалізація Apartment 10

Чому це працює? Тому що Apartment більше не тримає Person сильним посиланням. Цикл Person -> Apartment -> Person не утворюється: назад іде weak, отже коло не замикається.

3. unowned: посилання-обіцянка

У випадку unowned логіка інша. Воно теж не утримує об’єкт, тобто також допомагає розривати цикли. Але воно каже не «об’єкт може зникнути», а «об’єкт не зникне, доки я живий».

Тобто unowned — це не про «мені не шкода відпустити», а про «я точно знаю порядок життя об’єктів». І це знання стає частиною контракту вашого коду.

Технічний наслідок дуже помітний: unowned зазвичай не є Optional. А раз він не Optional, Swift не змушує вас перевіряти nil. Це зручно… доки контракт не порушено. Якщо ви звертаєтеся через unowned-посилання до вже звільненого об’єкта, програма падає. Це саме помилка часу виконання, а не «м’яка» поведінка.

У мові навіть є формулювання на рівні дизайну: для деяких контекстів існують варіанти unowned(safe) / unowned(unsafe) — тобто сама ідея «неутримувального посилання без Optional» доволі фундаментальна. У повсякденному коді, особливо в коді початківців, зазвичай використовують просто unowned.

Коли unowned доречний

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

Якщо дитина не може існувати без батька, weak створюватиме зайву optional-ність і засмічуватиме код перевірками. У такому випадку unowned може бути акуратнішим.

Мініприклад: батько–дитина через unowned

import Foundation

func demoUnowned() {
    final class Parent {
        let name: String
        var child: Child?
        init(name: String) { self.name = name; print("ініціалізація Parent \(name)") }
        deinit { print("деініціалізація Parent \(name)") }
    }

    final class Child {
        let name: String
        unowned let parent: Parent         // <-- не утримуємо, але гарантуємо "живий"
        init(name: String, parent: Parent) {
            self.name = name
            self.parent = parent
            print("ініціалізація Child \(name)")
        }
        deinit { print("деініціалізація Child \(name)") }
    }

    var p: Parent? = Parent(name: "P")
    if let parent = p {
        parent.child = Child(name: "C", parent: parent)
        print(parent.child?.parent.name ?? "немає батька")
        // P
    }
    p = nil
}

demoUnowned()
// Можливий вивід:
// ініціалізація Parent P
// ініціалізація Child C
// P
// деініціалізація Parent P
// деініціалізація Child C

Зверніть увагу на важливу ідею: у прикладі Parent тримає child сильно, отже дитина живе всередині життя батька. Тому дитині безпечно мати unowned-посилання на батька: контракт виконується.

4. Як вибрати між weak і unowned

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

Таблиця відмінностей: weak і unowned

Коли відмінності вже проговорені, корисно зафіксувати їх на одному екрані. У житті ви не будете щоразу перечитувати пояснення — ви згадуватимете саме таку таблицю.

Властивість
weak
unowned
Утримує об’єкт? Ні Ні
Може стати nil автоматично? Так Ні
Тип посилання Завжди Optional Зазвичай не Optional
Можна оголосити як let? Ні, лише var Так, часто let доречний
Що буде, якщо об’єкт уже звільнено? Ви побачите nil і маєте це обробити Падіння під час звернення: порушено контракт
Головна ідея «Об’єкт може зникнути — це нормально» «Об’єкт не зникне — я гарантую»

Ця таблиця напряму підводить до практичного правила вибору: weak — для «може не бути», unowned — для «має бути завжди».

Шпаргалка вибору: маленька схема

flowchart TD
    A[Потрібно розірвати цикл або зробити посилання неутримувальним] --> B{Чи може посилання законно стати порожнім?}
    B -->|Так, це нормальний сценарій| C[weak var x: T?]
    B -->|Ні, об’єкт має жити, доки живий я| D[unowned let x: T]

Якщо ви вагаєтеся — обирайте weak. Це безпечніше. unowned економить перевірки, але вимагає впевненості в життєвому циклі. Якщо впевненості немає, це не «оптимізація», а лотерея.

5. Приклад у стилі навчального застосунку: Library і Member

Щоб це не залишалося абстракцією про «людину й квартиру», давайте візьмемо приклад ближче до нашого курсу. Уявімо, що в нашому CLI-застосунку для керування бібліотекою з’являються класи для «сесії» або «контексту» застосунку. Нам потрібно зберігати учасників (читачів) і водночас у кожного читача мати посилання на бібліотеку — наприклад, щоб друкувати повідомлення з назвою бібліотеки або перевіряти правила.

Варіант із weak: читач може існувати окремо

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

import Foundation

func demoLibraryWithWeak() {
    final class Library {
        let name: String
        var members: [Member] = []
        init(name: String) { self.name = name; print("ініціалізація Library \(name)") }
        deinit { print("деініціалізація Library \(name)") }
    }

    final class Member {
        let name: String
        weak var library: Library?         // <-- бібліотека може зникнути/її може не бути
        init(name: String) { self.name = name; print("ініціалізація Member \(name)") }
        deinit { print("деініціалізація Member \(name)") }
    }

    var lib: Library? = Library(name: "City Library")
    var m: Member? = Member(name: "Sam")

    m?.library = lib
    lib?.members.append(m!)

    lib = nil
    m = nil
}

demoLibraryWithWeak()

Чому тут weak виглядає чесно? Тому що Member узагалі можна уявити без Library. Так, в ідеальному світі він завжди в бібліотеці, але код допускає перехідні стани. І weak підтримує такий дизайн.

Варіант із unowned: читач не може існувати без бібліотеки

Якщо ж за вашим доменним правилом Member створюється лише всередині Library і без неї не має сенсу, можна зробити контракт жорсткішим — через unowned. Код стане чистішим, але вимоги до життєвого циклу зростуть.

import Foundation

func demoLibraryWithUnowned() {
    final class Library {
        let name: String
        var members: [Member] = []
        init(name: String) { self.name = name; print("ініціалізація Library \(name)") }
        func addMember(name: String) {
            members.append(Member(name: name, library: self))
        }
        deinit { print("деініціалізація Library \(name)") }
    }

    final class Member {
        let name: String
        unowned let library: Library       // <-- має жити довше
        init(name: String, library: Library) {
            self.name = name
            self.library = library
            print("ініціалізація Member \(name)")
        }
        deinit { print("деініціалізація Member \(name)") }
    }

    var lib: Library? = Library(name: "Campus Library")
    lib?.addMember(name: "Alex")
    lib = nil
}

demoLibraryWithUnowned()

Зверніть увагу на архітектурний натяк: щоб unowned був чесним, корисно зробити так, щоб Member не можна було створити «сам по собі» без бібліотеки. Наприклад, через метод addMember або фабрику, а не через публічний init без контексту. Тоді контракт підтримує сам API, а не лише ваша віра в краще.

6. weak/unowned і синтаксис Swift

Коли ви почнете активно використовувати weak — особливо там, де змінна може стати nil, — ви зіткнетеся з тим, що Swift любить робити контроль потоку явним. Це видно навіть на рівні діагностики: коли self захоплено як weak, воно стає optional, і компілятор вимагає явного self? або явного розпакування.

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

І ще одна маленька пам’ятка з майбутнього досвіду: коли вам потрібно на певній ділянці коду тимчасово перетворити weak-посилання на сильне, у Swift є патерн optional binding. Він настільки поширений, що навіть обговорювався й закріплювався на рівні еволюції мови. Це не про магію, а про стиль: спочатку перевіряємо, що об’єкт живий, а потім працюємо з ним як із звичайним не-Optional. На практиці це найчастіше виглядає як if let

7. Типові помилки під час вибору weak / unowned

Помилка №1: використовувати unowned «щоб не писати ?», не маючи залізної гарантії життєвого циклу.
Це найчастіша пастка: ви дивитеся на weak var x: T?, бачите Optional, втомлюєтеся від if let, і рука тягнеться до unowned. Але unowned — це не косметика і не спосіб «зробити коротше». Це обіцянка. Якщо об’єкт може померти раніше, застосунок упаде. Тому, якщо ви хоча б на 1% не впевнені, що власник живе довше, weak чесніший.

Помилка №2: ставити weak там, де зв’язок обов’язковий за змістом, і перетворювати код на болото з Optional.
Іноді навпаки: розробник боїться падінь і починає ставити weak усюди підряд, навіть там, де зв’язок обов’язковий. Виходить дивна модель: об’єкт за змістом «не може жити без власника», але в коді власник чомусь Optional, і вам доводиться постійно робити перевірки, які за логікою ніколи не повинні спрацьовувати. У такій ситуації варто подумати про unowned і про те, щоб API не дозволяло створити об’єкт у неправильному стані.

Помилка №3: робити обидві сторони зв’язку weak, а потім дивуватися, що все раптово стало nil.
Якщо ви поставили weak з обох боків, ви справді розірвали цикл… але ще й прибрали володіння як факт. Тоді об’єкт може зникнути раніше, ніж ви очікували, бо його ніхто не тримає. У результаті ви ловите nil не як «рідкісний сценарій», а як постійний стан системи. Зазвичай у парі завжди має бути хоча б один власник (strong), інакше незрозуміло, хто відповідає за життя об’єкта.

Помилка №4: забувати, що weak — лише var.
Це проста, але часта помилка початківців: хочеться написати «посилання не володіє і не змінюється» та поставити let. Але weak змінюється автоматично — стає nil під час звільнення об’єкта, — тому weak let неможливий. Якщо вам потрібне «незмінне з вашої волі» посилання, але при цьому неутримувальне, це часто якраз випадок для unowned let, але тільки за чесного контракту життєвого циклу.

Помилка №5: намагатися поєднати weak/unowned з property wrappers і дивуватися помилкам компілятора.
Якщо ви дійдете до property wrappers, там є обмеження: властивості з wrapper’ом не можна оголошувати як weak або unowned. Це не каприз, а наслідок того, як обгортки розгортаються в збережені властивості. На практиці це означає: якщо вам потрібен weak/unowned, найімовірніше, це має бути звичайна stored property без wrapper’а.

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