JavaRush /Курсы /Swift SELF /Замыкания и захват self

Замыкания и захват self: capture list [ weak self ]

Swift SELF
37 уровень , 3 лекция
Открыта

1. Зачем говорить про захват переменных замыканием

Когда вы только привыкли к функциям, замыкания кажутся «функциями в кармане»: создал { ... }, передал куда-то — и оно выполнилось. Но у замыкания есть суперсила: оно помнит значения из внешней области видимости. Это удобно, потому что можно «упаковать» поведение вместе с нужными данными. И опасно, потому что замыкание может случайно удерживать объект дольше, чем вы рассчитывали.

Начнём с максимально безопасного примера: захват обычной переменной-числа. Здесь никакого ARC-ужаса не будет — просто почувствуем механику.

import Foundation

func makeGreeter() -> () -> Void {
    var counter = 0
    return {
        counter += 1
        print("Hello #\(counter)") // Hello #1, затем Hello #2, ...
    }
}

let greet = makeGreeter()
greet()
greet()

Замыкание захватило counter и продолжает его менять, хотя функция makeGreeter() давно закончилась. Это нормальное и полезное поведение.

Проблемы начинаются, когда замыкание захватывает ссылочный объект (class), особенно если этот объект сам хранит замыкание.

2. Как появляется цикл «объект → замыкание → объект»

Самый частый сценарий выглядит невинно: у объекта есть свойство-колбэк (замыкание), которое будет вызвано «когда-нибудь». Вы записываете туда { ... }, а внутри используете self, потому что «ну а кто ещё будет вызывать мои методы». И вот тут, без фанфар, вы можете собрать retain cycle.

Схема простая:

  • объект сильно хранит замыкание в своём свойстве;
  • замыкание сильно захватывает self (то есть объект);
  • получается круг, из которого ARC не умеет «догадаться, что вы хотели хорошего».

Это именно тот классический цикл self -> self.closure -> self, который часто приводят как пример утечки памяти.

Давайте сделаем учебный «кусок» для нашего консольного приложения (условно назовём его LibraryCLI). Представим, что есть объект «сессия», который хранит обработчик события.

import Foundation

final class LibrarySession {
    var onFinish: (() -> Void)?

    deinit {
        print("deinit LibrarySession")
    }

    func finish() {
        onFinish?()
    }
}

Теперь напишем контроллер, который владеет сессией и «подписывается» на завершение:

import Foundation

final class LibraryController {
    let session = LibrarySession()

    deinit {
        print("deinit LibraryController")
    }

    func start() {
        session.onFinish = {
            print("Session finished, controller reacts")
            self.cleanup()
        }
    }

    private func cleanup() {
        print("cleanup()")
    }
}

Выглядит логично. Но если вы создадите контроллер, вызовете start(), а потом «отпустите» внешнюю ссылку на контроллер — deinit может так и не случиться. Причина: LibraryController держит session, LibrarySession держит onFinish, а onFinish держит self (то есть LibraryController).

Можно представить это такой мини-схемой:

flowchart LR
    C[LibraryController] -->|strong| S[LibrarySession]
    S -->|strong| F[onFinish closure]
    F -->|captures strong| C

ARC смотрит на это и говорит: «Ну… у LibraryController точно есть сильная ссылка (внутри замыкания). Значит, живём дальше». И так «дальше» может продолжаться бесконечно, пока программа работает.

3. Capture list и [weak self]

Что такое capture list и где он стоит

Capture list — это список захватов в начале замыкания, который пишется в квадратных скобках перед in. Он существует ровно для того, чтобы вы могли явно управлять тем, как замыкание удерживает внешние значения.

Синтаксис (в общем виде) выглядит так:

{ [capture list] (params) -> ReturnType in
    // body
}

На практике чаще всего встречаются такие варианты:

Пример Что означает
{ [self] in ... }
Явно захватить self (сильно).
{ [weak self] in ... }
Захватить self слабо: не удерживать объект.
{ [unowned self] in ... }
Захватить self без удержания, но с жёсткой гарантией, что self жив.
{ [x] in ... }
Захватить значение x (обычно «зафиксировать» на момент создания).
{ [weak logger] in ... }
Слабо захватить другой объект logger.

Нам сегодня нужен самый популярный вариант: [weak self].

Важно: даже если вы ничего не пишете в capture list, замыкание всё равно может захватить self. Просто это будет происходить «по умолчанию» и визуально не бросаться в глаза, из-за чего цикл удержания получается особенно обидным: код выглядит как обычный, а память утекает как необычная.

Что делает [weak self] и почему после него self — это Optional

Когда вы пишете [weak self], вы просите замыкание не владеть объектом. То есть замыкание будет иметь «слабую ссылку» на объект: если объект исчезнет, ссылка автоматически станет nil. Это прямое продолжение того, что мы обсуждали про weak ссылки: они не удерживают объект и всегда опциональны.

Поэтому внутри такого замыкания self становится типа Self? (например, LibraryController?). И компилятор начинает честно требовать: «покажи, как ты обработаешь случай, когда self == nil».

То есть вот так писать нельзя (после [weak self]):

session.onFinish = { [weak self] in
    cleanup() // компилятор будет недоволен: неясно, что делать при self == nil
}

А вот так — можно, потому что мы явно описали поведение:

session.onFinish = { [weak self] in
    self?.cleanup()
}

Или через guard, что обычно читается ещё лучше (особенно когда логики больше пары строк).

4. Два стиля [weak self]: guard и self?.

С [weak self] у новичка обычно две эмоции. Первая: «Отлично, утечек не будет!» Вторая (через 10 секунд): «Почему теперь везде какие-то ? и guard, и почему компилятор не верит в мою любовь к аккуратности?»

Это нормальный этап. На практике в 90% случаев вы будете выбирать один из двух стилей: либо «если self умер — спокойно выходим», либо «выполняем отдельные действия, если self ещё жив». Разберём оба подхода, чтобы выбирать их осознанно, а не «как в первом попавшемся ответе в интернете».

Стиль guard let self else { return }

Этот стиль хорош, когда у вас есть кусок логики, который имеет смысл выполнять только если объект жив, и вы хотите, чтобы в середине выполнения self не «исчез» внезапно (например, чтобы не оказалось, что первую строчку вы выполнили, а дальше объект уже освобождён).

Паттерн выглядит так:

session.onFinish = { [weak self] in
    guard let self = self else { return }
    self.cleanup()
    print("Done")
}

Здесь важная мысль: после guard let self = self вы создаёте сильную локальную ссылку self (она «маскирует» слабую). На время выполнения замыкания объект становится удержанным этой локальной сильной ссылкой, а после выхода из замыкания — ссылка исчезает.

Также вы часто увидите более короткую форму:

session.onFinish = { [weak self] in
    guard let self else { return }
    cleanup()
}

Здесь self — это уже локальная константа (а не «захваченный self» напрямую), поэтому дальше можно писать вызовы без self.: cleanup() читается как self.cleanup().

Давайте применим это к нашему LibraryController, исправив цикл:

import Foundation

final class LibraryController {
    let session = LibrarySession()

    deinit { print("deinit LibraryController") }

    func start() {
        session.onFinish = { [weak self] in
            guard let self else { return }
            self.cleanup()
        }
    }

    private func cleanup() {
        print("cleanup()")
    }
}

Теперь замыкание не удерживает контроллер постоянно, и ARC сможет вызвать deinit, когда внешние ссылки на контроллер исчезнут.

Стиль self?.

Этот стиль хорош, когда логика внутри замыкания очень короткая, и нормально, если «ничего не произойдёт», когда объект уже исчез.

session.onFinish = { [weak self] in
    self?.cleanup()
}

Он компактный, но у него есть характерная ловушка: если внутри 5–6 строк, вам придётся писать self?. много раз, и код начинает выглядеть как «я не уверен ни в чём». Плюс иногда вам важно сделать всё или ничего, а self?. превращает выполнение в «может, сделаем половину, если повезёт» — это не всегда то, что вы хотите.

Если вы заметили, что self?. повторяется чаще двух раз — это хороший повод перейти на guard let self.

5. Полезные нюансы: вложенные замыкания и диагностика

Implicit self после unwrap и осторожность с вложенными closures

После того как вы сделали guard let self else { return }, можно писать вызовы методов без self. — то есть использовать implicit self. Это особенно приятно в коде с weak self, потому что иначе было бы слишком много «визуального шума».

Пример:

session.onFinish = { [weak self] in
    guard let self else { return }
    cleanup()          // это значит self.cleanup()
    print("Finished")
}

Но есть место, где нужно включать «режим осторожного программиста»: вложенные замыкания.

Представьте, что внутри вашего колбэка вы создаёте ещё одно замыкание и где-то его сохраняете/передаёте. Вложенные closures могут снова незаметно захватить self и создать новый цикл удержания. Поэтому у вложенных замыканий стоит явно контролировать захват: либо снова писать capture list, либо явно писать self. — чтобы читателю было видно, что именно происходит.

Учебный пример-«маячок» (здесь важна идея, а не полезность):

func configure() {
    session.onFinish = { [weak self] in
        guard let self else { return }

        let later = { [weak self] in
            guard let self else { return }
            self.cleanup()
        }

        later()
    }
}

Да, это чуть длиннее. Зато читатель кода сразу видит: «ага, тут ещё одно замыкание, и оно тоже управляет захватом self».

Мини-диагностика: как проверить, что [weak self] реально помог

Очень хочется, чтобы после добавления [weak self] всё стало хорошо «по умолчанию». Но на практике бывает так: вы поставили [weak self] в одном месте, а цикл всё ещё живёт через другое свойство, коллекцию или ещё одно замыкание.

Самый простой детектор в учебных примерах — print в deinit. Мы этим уже пользуемся, и это абсолютно нормальная техника на первых этапах.

Давайте соберём мини-демо в top-level коде, чтобы увидеть разницу глазами:

import Foundation

func demo() {
    var controller: LibraryController? = LibraryController()
    controller?.start()

    controller?.session.finish()

    controller = nil
    print("controller is nil now")
}

demo()

Если всё сделано правильно, вы должны увидеть, что после controller = nil (или сразу после выхода из demo) срабатывает deinit LibraryController и deinit LibrarySession.

Если не срабатывает, то это почти всегда означает: «где-то осталась сильная ссылка». Частая причина — сохранённое замыкание, которое держит объект через захват self.

И ещё практический совет: если вы храните замыкание в свойстве и оно нужно только временно, иногда полезно «отписаться» после использования, присвоив nil:

session.onFinish = { [weak self] in
    guard let self else { return }
    self.cleanup()
    self.session.onFinish = nil   // снимаем подписку
}

Это не «замена weak self», а дополнительная гигиена: вы убираете лишние связи, и граф ссылок становится проще.

6. Типичные ошибки при захвате self и использовании [weak self]

Первые ошибки здесь обычно не потому, что студент «невнимательный», а потому что мозг ещё не привык думать графом ссылок. Вы пишете код как историю действий, а ARC живёт как бухгалтер: «кто на кого ссылается и кто кого удерживает». Давайте аккуратно пройдёмся по самым частым граблям, чтобы вы узнавали их по звуку ещё до падения.

Ошибка №1: поставить [weak self], а потом написать self! “потому что иначе не компилируется”.
Такое встречается часто: человек увидел, что self стал Optional, и решил «да ну его, я уверен». Проблема в том, что weak self как раз говорит: «объект может исчезнуть». Если вы затем делаете self!, вы превращаете исчезновение объекта в потенциальный крэш. Правильная привычка — либо guard let self else { return }, либо self?..

Ошибка №2: использовать self?. там, где логика должна выполниться целиком или не выполниться вообще.
self?.step1(); self?.step2(); self?.step3() выглядит компактно, но это “мягкая” стратегия: если self стал nil между строками (или у вас появятся дополнительные условия), вы можете получить частичное выполнение. Когда важна цельная логика, guard let self делает намерение гораздо яснее.

Ошибка №3: не заметить второй цикл через вложенное замыкание.
Вы починили основной колбэк: добавили [weak self]. А внутри этого колбэка создали ещё одно замыкание и где-то его сохранили/передали — и оно уже захватило self сильно. Вложенные замыкания требуют особого внимания: там особенно важно, чтобы захват был заметным в коде.

Ошибка №4: выбрать unowned “потому что так короче”.
Коротко — да, безопасно — не всегда. unowned требует реального контракта: объект гарантированно жив, когда вы обращаетесь к ссылке. Если такой гарантии нет, лучше weak. Мы здесь фокусируемся на [weak self] именно потому, что для новичка это наиболее безопасная стратегия по умолчанию.

Ошибка №5: ждать, что deinit вызовется “сразу после последнего использования”, а не после исчезновения последней сильной ссылки.
Это тонкая психологическая ловушка. Вы “перестали использовать” объект, но где-то в коллекции, свойстве или замыкании ещё осталась сильная ссылка — и ARC честно держит объект живым. Поэтому deinit — это не награда за хорошее поведение, а просто сигнал: “сильных ссылок больше нет”.

1
Задача
Swift SELF, 37 уровень, 3 лекция
Недоступна
Приветствие счётчика
Приветствие счётчика
1
Задача
Swift SELF, 37 уровень, 3 лекция
Недоступна
Финиш без утечки
Финиш без утечки
1
Задача
Swift SELF, 37 уровень, 3 лекция
Недоступна
Завершение загрузки
Завершение загрузки
1
Задача
Swift SELF, 37 уровень, 3 лекция
Недоступна
События экрана
События экрана
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ