"Weak" и "Unowned" в чем разница
В Swift (и других языках с автоматическим подсчётом ссылок, например, Objective-C) существует риск цикла сильных ссылок (strong reference cycle), при котором два или более объекта ссылаются друг на друга, не позволяя системе освободить память. Чтобы предотвратить утечки памяти, Swift предоставляет два механизма ослабленных ссылок: weak и unowned. Оба они не увеличивают счётчик ссылок (reference count), но работают по-разному в управлении временем жизни и безопасности.
📌 Общие сведения о ссылках
-
Strong (сильная) — по умолчанию: увеличивает счётчик ссылок объекта. Пока хотя бы одна сильная ссылка указывает на объект — он жив.
-
Weak (слабая) — не увеличивает счётчик ссылок. Объект может быть уничтожен, и ссылка автоматически становится nil.
-
Unowned (безвладельческая) — также не увеличивает счётчик, но не допускает значения nil и не проверяет, жив ли объект.
🔄 Проблема strong reference cycle
Представим два объекта A и B, которые хранят ссылки друг на друга:
class A {
var b: B?
}
class B {
var a: A?
}
Если a.b и b.a — strong ссылки, они никогда не освободятся из памяти, потому что у них всегда будет хотя бы один активный счётчик ссылок.
✅ Решение: слабые (weak) и безвладельческие (unowned) ссылки
Оба вида ссылки не увеличивают reference count, но ведут себя по-разному при удалении объекта.
🧷 weak (слабая ссылка)
Ключевые характеристики:
-
Может быть nil.
-
Автоматически становится nil, когда объект освобождён из памяти.
-
Объявляется только как опциональный тип (SomeClass?).
-
Безопасна: доступ к уничтоженному объекту невозможен, потому что ссылка становится nil.
Используется, когда:
-
Ссылающийся объект может не существовать во время всего жизненного цикла.
-
Возникает возможность временного отсутствия значения.
Пример:
class Person {
var name: String
init(name: String) { self.name = name }
var pet: Pet?
}
class Pet {
var name: String
weak var owner: Person? // предотвращает цикл
init(name: String) { self.name = name }
}
Когда Person освобождается, ссылка owner автоматически становится nil, и Pet больше не будет удерживать Person в памяти.
🧷 unowned (безвладельческая ссылка)
Ключевые характеристики:
-
Не может быть nil (в отличие от weak).
-
Не проверяет, жив ли объект.
-
Если объект уже уничтожен и по ссылке unowned идёт обращение — произойдёт краш (EXC_BAD_ACCESS).
-
Используется для жёстко связанных объектов, где объект-собственник живёт не меньше, чем тот, кто на него ссылается.
Используется, когда:
-
Ссылающийся объект обязан существовать столько же, сколько объект с unowned-ссылкой.
-
Необходима неопциональная ссылка, без риска захвата.
Пример:
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // предполагается, что customer жив до тех пор, пока карта жива
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
}
В этом случае CreditCard предполагает, что Customer точно будет существовать, пока существует сама карта. Если Customer будет уничтожен раньше — обращение к customer приведёт к крашу.
📋 Сравнение weak и unowned
Свойство | weak | unowned |
---|---|---|
Увеличивает reference count | ❌ | ❌ |
--- | --- | --- |
Может быть nil | ✅ | ❌ |
--- | --- | --- |
Автоматически обнуляется | ✅ | ❌ |
--- | --- | --- |
Тип | Опциональный (?) | Неопциональный |
--- | --- | --- |
Безопасность доступа | Безопасный (nil) | Небезопасный (краш при доступе) |
--- | --- | --- |
Часто используется при | Обратных ссылках, delegate | Внутренних связях «вниз» |
--- | --- | --- |
💡 Когда использовать какой
Используй weak, если:
-
Ссылка может быть обнулена.
-
Ссылка на делегата (delegation pattern), особенно в iOS (например, delegate в UITableView).
-
Не уверен в жизненном цикле объектов.
protocol SomeDelegate: AnyObject {
func didSomething()
}
class MyController {
weak var delegate: SomeDelegate? // класс может уйти из памяти
}
Используй unowned, если:
-
Ссылка должна всегда существовать во время жизни объекта.
-
Требуется избежать опциональности, но сохранить слабую ссылку.
-
Используется в замыканиях (closures), чтобы избежать захвата self как strong-ссылки.
class ViewModel {
var onUpdate: (() -> Void)?
func bind() {
onUpdate = { \[unowned self\] in
self.updateUI()
}
}
}
🔁 Использование в замыканиях
Проблема:
Замыкания (closures) по умолчанию захватывают переменные сильно, что может привести к retain cycle.
Решение:
class ViewController {
var onAction: (() -> Void)?
func setup() {
onAction = { \[weak self\] in
self?.doSomething()
}
}
}
Если использовать [unowned self] — замыкание не увеличит счётчик ссылок, но если self будет уничтожен к моменту вызова, произойдёт краш.
🧪 Пример с жизненным циклом
class A {
var b: B?
deinit { print("A deallocated") }
}
class B {
weak var a: A? // если заменить на strong — будет утечка
deinit { print("B deallocated") }
}
func test() {
var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
a = nil
b = nil
}
Если B.a будет strong, то ни A, ни B не будут освобождены. С weak всё освобождается корректно.
🧠 Закулисные детали (реализация в Swift)
-
Swift добавляет механизм автоматического обнуления для weak ссылок, используя внутренний механизм side table, в котором отслеживаются все слабые ссылки на объект.
-
Для unowned ссылка остаётся валидной, пока объект не удалён. После удаления объекту присваивается специальный «мусорный» адрес, и при попытке обращения происходит EXC_BAD_ACCESS.
📎 Использование с @IBOutlet
В iOS/SwiftUI при связывании интерфейса с кодом через @IBOutlet, ссылка объявляется как weak:
@IBOutlet weak var label: UILabel!
Это связано с тем, что UI-объекты уже удерживаются иерархией вида (view hierarchy), и чтобы избежать retain cycle, IBOutlet-ссылки делают weak.