В чем разница между weak и unowned ссылками в Swift при управлении памятью?
Ключевое различие: weak vs unowned
Разница между weak и unowned сводится к двум вещам: опциональность и гарантия времени жизни.
1. Weak ссылки (слабые ссылки)
weak — это всегда опциональный тип. Когда объект, на который указывает weak-ссылка, освобождается из памяти, ARC автоматически устанавливает эту ссылку в nil. Это безопасно, но требует разворачивания.
Когда использовать:
- Когда объект может быть освобожден раньше, чем ссылающийся на него объект.
- Когда связь между объектами необязательна (например, делегат может отсутствовать).
- Типичный пример:
delegateв UIKit — делегат может быть освобожден, и мы не хотим крешнуться.
2. Unowned ссылки (бесхозные ссылки)
unowned — это неопциональный тип. Он предполагает, что объект будет жить как минимум столько же, сколько и ссылающийся на него объект. Если объект освобождается раньше, обращение к unowned-ссылке вызовет runtime crash.
Когда использовать:
- Когда вы абсолютно уверены, что объект не будет освобожден до того, как ссылающийся объект исчезнет.
- Когда связь обязательна — например, у человека всегда есть сердце, и сердце не может существовать без человека.
- Типичный пример: замыкания, где
selfгарантированно существует на момент выполнения замыкания.
Решение вашей проблемы
В вашем примере связь между Profile и Avatar взаимная, но не обязательная для обеих сторон. Profile может существовать без Avatar, и Avatar может существовать без Profile? В реальности — да, аватарка может быть не установлена. Поэтому логично сделать weak для свойства profile в Avatar:
class Avatar {
let imageName: String
weak var profile: Profile? // Теперь не создает цикл
init(imageName: String) {
self.imageName = imageName
}
deinit {
print("Avatar \(imageName) deallocated")
}
}
Теперь при освобождении Profile, avatar?.profile автоматически станет nil, и цикл разорвется.
Когда использовать unowned: пример с замыканием
Рассмотрим ситуацию, когда объект гарантированно переживает замыкание:
class DataLoader {
var completionHandler: (() -> Void)?
func loadData() {
// Здесь self точно существует, пока замыкание не выполнено
completionHandler = { [unowned self] in
self.processData() // Безопасно, т.к. self жив
}
}
func processData() {
print("Processing...")
}
}
Если бы мы использовали weak self, пришлось бы писать self?.processData(), что менее удобно. Но если DataLoader будет освобожден до вызова замыкания — получим crash. Поэтому unowned — это обещание компилятору: "Я знаю, что делаю".
Практические рекомендации
- Всегда начинайте с weak, если сомневаетесь. Безопасность важнее удобства.
- Используйте unowned только когда:
- Объект не может быть nil (например, в иерархиях, где родитель гарантированно живет дольше ребенка).
- В замыканиях, когда вы точно знаете, что self не будет освобожден до выполнения замыкания (например, при асинхронных операциях с коротким временем жизни).
- Избегайте unowned в публичных API — другой разработчик может нарушить ваше предположение о времени жизни.
Итог
weak — безопасно, но неудобно (опционал). unowned — удобно (не опционал), но опасно (crash). Выбирайте по контексту: если есть хоть малейшая вероятность, что объект умрет раньше — используйте weak.
Дополнение: Unowned vs Weak в замыканиях
Есть еще один важный нюанс: в замыканиях unowned self часто используется для краткости, но это может привести к трудноуловимым багам. Рассмотрим пример:
class ViewController {
var button: UIButton!
func setupButton() {
button.addTarget(self, action: #selector(didTap), for: .touchUpInside)
}
@objc func didTap() {
// Асинхронная операция
DispatchQueue.global().async { [unowned self] in
// Если ViewController уже освобожден к этому моменту — crash!
self.updateUI()
}
}
func updateUI() {
print("UI updated")
}
}
Если пользователь уйдет с экрана до того, как замыкание выполнится, приложение упадет. Поэтому лучшая практика — использовать weak self с guard-разворачиванием:
DispatchQueue.global().async { [weak self] in
guard let self = self else { return }
self.updateUI()
}
Этот паттерн безопасен и явно показывает, что мы проверяем существование объекта. Да, код чуть длиннее, но он предотвращает креши. Помните: безопасность кода важнее его краткости.