Как Swift управляет временем жизни переменной, захваченной escaping-замыканием?
Escaping-замыкание сохраняет захваченное окружение после выхода из функции, поэтому нужные переменные остаются доступны до тех пор, пока живо замыкание. При захвате изменяемой переменной замыкание обычно работает с сохранённым хранилищем этой переменной, а не с разовым текстовым снимком её значения.
Замыкания нужны, чтобы передавать поведение как значение: например, обработчик события, завершения асинхронной операции или критерий обработки коллекции. Для этого замыкание должно иметь доступ к переменным из внешней области видимости даже после завершения функции, в которой оно создано.
Swift явно различает замыкания, вызываемые только внутри функции, и замыкания, которые могут пережить её выполнение. Для параметра, сохраняемого или вызываемого позже, используется аннотация @escaping. Она делает потенциально длительное время жизни захваченного окружения явным для разработчика и компилятора.
Если функция создаёт замыкание, а затем возвращает его или сохраняет в объекте, локальные переменные функции обычно уже не должны уничтожаться сразу после возврата. Иначе отложенный вызов обратился бы к несуществующим данным.
Неверное предположение о захвате приводит к ошибкам: разработчик может ожидать независимые копии значений, получить общее изменяемое состояние, удержать объект дольше нужного или создать цикл сильных ссылок.
При захвате Swift формирует контекст замыкания — сохраненное окружение, содержащее необходимые внешние значения или хранилища переменных. Этот контекст живёт не меньше самого замыкания. Поэтому выход исходной функции не завершает время жизни захваченных данных.
Для неизменяемого значения поведение часто выглядит как захват текущего значения. Для изменяемой локальной переменной важен другой аспект: несколько замыканий, захвативших одну такую переменную, могут обращаться к общему сохранённому состоянию. Изменение через одно замыкание будет видно другому.
Если нужен именно снимок значения на момент создания замыкания, применяют capture list. Она позволяет явно зафиксировать захваченное значение или задать слабую либо безусловно слабую ссылку на объект.
Ссылки на экземпляры классов по умолчанию захватываются сильно. Поэтому замыкание, сохранённое свойством объекта, может образовать цикл: объект сильно удерживает замыкание, а замыкание сильно удерживает объект через захваченную ссылку self. Для разрыва цикла используют weak или unowned, выбирая вариант с учетом гарантии времени жизни объекта.
Продление жизни переменной не означает, что замыкание безопасно для конкурентного доступа. Если состояние изменяется из разных потоков или задач, отдельно нужны синхронизация и соблюдение правил конкурентности Swift.
После возврата из makeCounter переменная value сохраняется в окружении замыкания. Последующие вызовы next используют одно и то же сохранённое состояние, поэтому результат увеличивается.
В экранном контроллере замыкание завершения асинхронного запроса сохранялось сетевым объектом. Замыкание обращалось к self, а контроллер удерживал сетевой объект. В результате экран не освобождался после закрытия: возник цикл сильных ссылок.
Рассматривались два варианта. Сильный захват был прост, но создавал утечку; unowned self разрывал цикл без optional-значения, но приводил бы к аварийному завершению, если запрос завершится после уничтожения контроллера.
Выбрали захват weak self с безопасной проверкой существования объекта внутри замыкания. Это устранило цикл и позволило корректно проигнорировать результат, если экран уже закрыт. Компромисс — необходимость обработать отсутствие self и не считать контроллер гарантированно живым.
1. Чем захват значения через capture list отличается от обычного захвата изменяемой переменной?
Capture list фиксирует выражение в момент создания замыкания. Последующие изменения исходной переменной не изменяют уже захваченную копию. При обычном захвате изменяемой локальной переменной замыкание может работать с сохранённым общим хранилищем, поэтому его последующие изменения наблюдаемы при следующих вызовах.
2. Всегда ли weak self достаточно для безопасного замыкания?
Нет. weak предотвращает сильное удержание объекта, но ссылка может стать nil в любой момент. Замыкание должно корректно обработать этот случай. Кроме того, если внутри замыкания временно получить сильную локальную ссылку на self, объект будет жив до конца конкретного вызова, что обычно необходимо для согласованности операции.
3. Почему захват переменной не решает проблему гонок данных?
Сохранённое хранилище продлевает жизнь состояния, но не делает доступ к нему атомарным и не синхронизирует разные исполнители. Одновременное чтение и изменение такого состояния из конкурентных задач может привести к гонке или нарушению правил Swift Concurrency. Для решения нужны изоляция actor, корректная передача Sendable-данных или иной механизм синхронизации.