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("init Person \(name)") }
deinit { print("deinit Person \(name)") }
}
final class Apartment {
let number: Int
weak var tenant: Person? // <-- не удерживаем жильца
init(number: Int) { self.number = number; print("init Apt \(number)") }
deinit { print("deinit Apt \(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 может отличаться):
// init Person Ann
// init Apt 10
// deinit Person Ann
// deinit Apt 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 будет создавать лишнюю «опциональность» и засорять код проверками. В этом случае unowned может быть аккуратнее.
Мини‑пример: родитель–ребёнок через unowned
import Foundation
func demoUnowned() {
final class Parent {
let name: String
var child: Child?
init(name: String) { self.name = name; print("init Parent \(name)") }
deinit { print("deinit Parent \(name)") }
}
final class Child {
let name: String
unowned let parent: Parent // <-- не удерживаем, но обещаем "жив"
init(name: String, parent: Parent) {
self.name = name
self.parent = parent
print("init Child \(name)")
}
deinit { print("deinit Child \(name)") }
}
var p: Parent? = Parent(name: "P")
if let parent = p {
parent.child = Child(name: "C", parent: parent)
print(parent.child?.parent.name ?? "no parent")
// P
}
p = nil
}
demoUnowned()
// Возможный вывод:
// init Parent P
// init Child C
// P
// deinit Parent P
// deinit 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("init Library \(name)") }
deinit { print("deinit Library \(name)") }
}
final class Member {
let name: String
weak var library: Library? // <-- библиотека может исчезнуть/не быть
init(name: String) { self.name = name; print("init Member \(name)") }
deinit { print("deinit 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("init Library \(name)") }
func addMember(name: String) {
members.append(Member(name: name, library: self))
}
deinit { print("deinit Library \(name)") }
}
final class Member {
let name: String
unowned let library: Library // <-- обязана жить дольше
init(name: String, library: Library) {
self.name = name
self.library = library
print("init Member \(name)")
}
deinit { print("deinit 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’а.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ