Когда и зачем использовать weak при захвате self в замыкании в Swift?

Ответ

Ключевое слово weak используется в списке захвата замыкания для предотвращения цикла сильных ссылок (retain cycle).

Когда это необходимо? Используйте [weak self], когда:

  1. Замыкание хранится как свойство (например, колбэк, обработчик).
  2. self владеет этим замыканием (прямо или косвенно).
  3. Замыкание может пережить self (является @escaping).

Пример проблемы и решения:

class DataLoader {
    var onComplete: (() -> Void)? // Хранимое escaping-замыкание

    func loadData() {
        // БЕЗ weak: ЦИКЛ! DataLoader владеет onComplete, а замыкание сильно ссылается на DataLoader.
        // onComplete = { self.handleData() } // Память никогда не освободится.

        // С weak: Цикл разорван.
        onComplete = { [weak self] in
            // self стал опциональным из-за weak
            self?.handleData()
        }
    }
    func handleData() { print("Data processed") }
    deinit { print("DataLoader deallocated") }
}

Альтернатива unowned: Используйте [unowned self], только если вы абсолютно уверены, что замыкание не переживет self. В противном случае это приведет к аварийному завершению.

// Допустимо, если замыкание выполняется синхронно в рамках жизни self.
dispatchQueue.async { [unowned self] in
    self.updateUI() // Crash, если self к этому моменту освобожден!
}

Лучшая практика: По умолчанию используйте weak для @escaping замыканий и безопасно разворачивайте self с помощью guard:

onComplete = { [weak self] in
    guard let self = self else { return }
    self.handleData() // self теперь strong reference на время выполнения замыкания
}

Ответ 18+ 🔞

А, слушай, вот этот твой weak в замыканиях — это вообще отдельная песня, блядь. Представь себе такую пиздопроебибну: два объекта держат друг друга за жопу, и никто из них не может умереть. Классический retain cycle, ёпта.

Когда эта хрень нужна? Пиши [weak self], когда:

  1. Замыкание — это свойство класса, типа колбэка или обработчика. Оно, сука, может жить своей жизнью.
  2. Сам объект (наш self) владеет этим замыканием. Получается взаимная любовь, блядь.
  3. Замыкание — @escaping, то есть оно может сбежать и жить дольше, чем сам объект. Вот тут-то и пиздец.

Смотри, как это выглядит на практике:

class DataLoader {
    var onComplete: (() -> Void)? // Хранимое escaping-замыкание

    func loadData() {
        // БЕЗ weak: ЕБАНЫЙ ЦИКЛ! DataLoader держит onComplete, а замыкание держит DataLoader.
        // onComplete = { self.handleData() } // Память нахуй не освободится никогда, welcome to memory leak.

        // С weak: Цикл порвали, как Тузик грелку.
        onComplete = { [weak self] in
            // self стал опциональным, потому что ссылка слабая, ёпта
            self?.handleData()
        }
    }
    func handleData() { print("Data processed") }
    deinit { print("DataLoader deallocated") }
}

А есть же ещё unowned, эта хитрая жопа. [unowned self] — это как похуистичная версия weak. Используй её, только если ты на 146% уверен, что замыкание точно умрёт раньше или вместе с self. Иначе — краш, пиздец, и ты будешь искать, кто тебя так научил.

// Ну допустим, если замыкание выполняется тут же и сразу.
dispatchQueue.async { [unowned self] in
    self.updateUI() // CRASH, если self к этому моменту уже в тапки дунул!
}

Лайфхак от бывалых: Просто бери weak для всех @escaping замыканий по умолчанию. И разворачивай его красиво через guard, чтобы не писать self?. в каждой строчке:

onComplete = { [weak self] in
    guard let self = self else { return } // Нет self — нет и проблем, в рот меня чих-пых.
    self.handleData() // А тут self уже сильная ссылка, но только на время работы замыкания.
}

Вот и вся магия, блядь. Не создавай себе циклов, живи спокойно.