Avatar ·

В чем разница между weak и unowned ссылками в Swift при управлении памятью?

Всем привет! Я изучаю Swift и пытаюсь разобраться с ARC (Automatic Reference Counting). Прочитал документацию, но никак не могу понять разницу между weak и unowned ссылками. Обе они вроде бы не увеличивают счетчик ссылок, но в чем тогда отличие? У меня есть такой пример. Я пишу простое приложение с профилем пользователя и его аватаром. Если пользователь уходит с экрана, я хочу, чтобы объект Profile и его Avatar освобождались из памяти. Но я получаю утечку памяти из-за цикла сильных ссылок. Вот мой упрощенный код: ```swift class Profile { let name: String var avatar: Avatar? init(name: String) { self.name = name print("Profile \(name) created") } deinit { print("Profile \(name) deallocated") } } class Avatar { let imageName: String var profile: Profile? // Вот здесь проблема init(imageName: String) { self.imageName = imageName print("Avatar \(imageName) created") } deinit { print("Avatar \(imageName) deallocated") } } var profile: Profile? = Profile(name: "John") profile?.avatar = Avatar(imageName: "avatar.jpg") profile?.avatar?.profile = profile // Цикл! ``` Я понимаю, что нужно использовать weak или unowned для свойства profile в Avatar, но не знаю, что выбрать. Когда я использую weak, я получаю опционал, и мне приходится его разворачивать. А unowned — это неопциональный тип, но я боюсь, что получу crash, если объект уже освобожден. Объясните, пожалуйста, на конкретных примерах, когда применять weak, а когда unowned. И как избежать висячих ссылок (dangling pointers) при использовании unowned? Заранее спасибо! <<>>

Ключевое различие: 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()
}

Этот паттерн безопасен и явно показывает, что мы проверяем существование объекта. Да, код чуть длиннее, но он предотвращает креши. Помните: безопасность кода важнее его краткости.