Avatar ·

Нужно ли всегда использовать [unowned self] в замыканиях в Swift?

Всем привет! Я начал изучать Swift и столкнулся с проблемой управления памятью в замыканиях. Читал много статей на Stack Overflow, и везде пишут, что нужно использовать `[weak self]` или `[unowned self]`, чтобы избежать retain cycles. Но я запутался, когда именно применять `unowned`. Вот пример: у меня есть класс `NetworkManager`, который загружает данные и вызывает замыкание после завершения. ```swift class NetworkManager { func fetchData(completion: @escaping (Data?) -> Void) { DispatchQueue.global().async { // имитация загрузки sleep(2) let data = Data() completion(data) } } } class ViewController: UIViewController { let networkManager = NetworkManager() func loadData() { networkManager.fetchData { data in // обрабатываем данные self.updateUI(with: data) } } func updateUI(with data: Data?) { // обновление интерфейса } } ``` Мне сказали, что здесь может быть retain cycle, потому что замыкание захватывает `self` (ViewController), а ViewController держит сильную ссылку на networkManager, который в свою очередь держит замыкание. Но я не понимаю: если я использую `[unowned self]`, то приложение может упасть, если `self` уже освобождён. А если `[weak self]` — то нужно писать `guard let self = self else { return }`, что выглядит громоздко. Подскажите, пожалуйста, нужно ли всегда использовать `[unowned self]`? Или есть случаи, когда можно обойтись без него? Как правильно выбирать между `weak` и `unowned`? Может быть, есть какие-то правила или best practices? Заранее спасибо! <<>>

Краткий ответ: нет, не всегда. Выбор между weak, unowned и отсутствием модификатора зависит от времени жизни объектов.

Давайте разберёмся по порядку. Retain cycle возникает, когда два объекта держат сильные ссылки друг на друга, и ни один из них не может быть освобождён. В вашем примере:

  • ViewController держит сильную ссылку на NetworkManager
  • NetworkManager держит сильную ссылку на замыкание (через completion)
  • Замыкание держит сильную ссылку на self (ViewController)

Это классический треугольный retain cycle. Если вы не используете [weak self] или [unowned self], ViewController никогда не будет освобождён, даже если вы его закроете.

Когда использовать [unowned self]?

Используйте unowned, когда вы абсолютно уверены, что self будет жить как минимум столько же, сколько и замыкание. Например:

class MyViewController: UIViewController {
    let label = UILabel()
    
    func setupLabel() {
        // Замыкание выполняется синхронно, self точно жив
        let action = { [unowned self] in
            self.label.text = "Hello"
        }
        action()
    }
}

Или когда замыкание вызывается внутри метода того же объекта:

class DataProcessor {
    var data: [Int] = []
    
    func process() {
        someAsyncOperation { [unowned self] in
            // self точно жив, пока выполняется process()
            self.data.append(1)
        }
    }
}

Важно: если self будет освобождён до выполнения замыкания, приложение упадёт с EXC_BAD_ACCESS.

Когда использовать [weak self]?

Используйте weak в большинстве случаев, особенно когда:

  • Замыкание может пережить self (асинхронные операции, сетевые запросы)
  • self — UIViewController, который может быть закрыт
  • Вы не уверены в порядке освобождения объектов
class ViewController: UIViewController {
    func loadData() {
        networkManager.fetchData { [weak self] data in
            guard let self = self else { return }
            self.updateUI(with: data)
        }
    }
}

Да, guard let self = self выглядит громоздко, но это безопасно. В Swift 4.2+ можно использовать короткую запись:

networkManager.fetchData { [weak self] data in
    self?.updateUI(with: data)
}

Но будьте осторожны: если нужно выполнить несколько действий с self, лучше использовать guard, чтобы не писать self? каждый раз.

Когда можно вообще не использовать weak/unowned?

Только если замыкание не захватывает self или захватывает его как слабую ссылку по умолчанию. Например:

// Нет захвата self — нет проблем
let closure = {
    print("Hello")
}

// Захватываем свойство, а не self
class MyClass {
    var value = 5
    func doSomething() {
        let closure = { [value] in
            print(value) // захвачена копия value
        }
    }
}

Best Practices:

  1. По умолчанию используйте weak — это безопаснее всего
  2. Используйте unowned только когда уверены в жизненном цикле — например, в синхронных замыканиях или в замыканиях, которые гарантированно выполняются до освобождения self
  3. Избегайте unowned в UI-контроллерах — они могут быть освобождены в любой момент (пользователь закрыл экран)
  4. Проверяйте retain cycles с помощью инструментов Xcode — Debug Memory Graph

В вашем примере с NetworkManager обязательно используйте [weak self], потому что сетевой запрос может завершиться после того, как ViewController уже закрыт.

<<>>

Альтернативный подход: используйте [weak self] с опциональной цепочкой

Многие разработчики предпочитают не писать guard let self = self, а использовать опциональную цепочку:

networkManager.fetchData { [weak self] data in
    self?.updateUI(with: data)
    self?.anotherMethod()
    self?.yetAnotherMethod()
}

Это короче и читается легче. Но есть нюанс: если self станет nil после первого вызова, остальные методы не выполнятся. Для простых случаев это нормально, но если логика требует атомарности (все методы должны выполниться или ни один), лучше использовать guard.

Ещё один совет: можно создать локальную strong ссылку внутри замыкания, чтобы гарантировать, что self жив на время выполнения:

networkManager.fetchData { [weak self] data in
    guard let strongSelf = self else { return }
    strongSelf.updateUI(with: data)
    strongSelf.anotherMethod()
}

Некоторые разработчики называют это "weak-strong dance". Это безопасно, потому что strongSelf — локальная переменная, и она будет жить только внутри замыкания.

В современных версиях Swift (5.0+) также можно использовать [weak self] в сочетании с guard let self, что стало идиоматичным:

networkManager.fetchData { [weak self] data in
    guard let self else { return }
    // self теперь strong ссылка внутри этого блока
    self.updateUI(with: data)
}

Этот синтаксис (без повторения self в guard) появился в Swift 5.0 и делает код чище.