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
Коли відмінності вже проговорені, корисно зафіксувати їх на одному екрані. У житті ви не будете щоразу перечитувати пояснення — ви згадуватимете саме таку таблицю.
| Властивість | |
|
|---|---|---|
| Утримує об’єкт? | Ні | Ні |
| Може стати 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’а.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ