Захватывая изменяемую локальную переменную в escaping-замыкании, что именно сохраняет замыкание — её значение или общее хранилище?
Escaping-замыкание захватывает не снимок значения изменяемой локальной переменной, а общее хранилище этой переменной. Поэтому несколько замыканий видят изменения одной и той же переменной, а её хранилище продолжает жить после возврата функции, пока оно нужно замыканиям.
Если переменная содержит сильную ссылку на объект, это хранилище также удерживает текущий объект. При присваивании нового значения старая сильная ссылка освобождается, если другие владельцы отсутствуют.
Локальные переменные обычно связаны с временем выполнения функции, однако escaping-замыкание может быть вызвано после её завершения. Для поддержки такого поведения Swift переносит необходимые захваченные данные в контекст замыкания, время жизни которого управляется ARC.
Это решает проблему доступа к уже завершившемуся стековому кадру. Вместо обращения к уничтоженной локальной памяти замыкание использует сохранённое хранилище, существующее независимо от исходного вызова функции.
Рассмотрим функцию, которая создаёт два замыкания: одно читает переменную, другое изменяет её. Если считать, что каждое замыкание получило отдельную копию, можно ошибочно ожидать старое значение.
На практике оба замыкания могут обращаться к одному хранилищу. Неверное понимание приводит к неожиданным результатам, удержанию объектов дольше ожидаемого и ошибкам при проектировании callback-цепочек.
Особенно важно различать захват изменяемой переменной и захват значения через capture list. В первом случае изменения видны другим замыканиям, во втором создаётся отдельный захваченный экземпляр значения на момент формирования замыкания.
Семантически изменяемая захваченная переменная представлена общим состоянием. Несколько замыканий, ссылающихся на неё, читают и изменяют это состояние, поэтому присваивание из одного замыкания доступно другим.
Для escaping-замыкания это состояние должно пережить возврат функции. Реализация обычно размещает его в специальном объектоподобном контейнере, часто называемом box, но прикладной код должен опираться на семантику общего хранилища, а не на детали реализации.
После возврата makeActions переменная worker уже не живёт как обычная локальная переменная, но её захваченное хранилище сохраняется. Вызов clear записывает в это хранилище nil, освобождая сильную ссылку на Worker; затем read получает nil.
Список захвата меняет модель. Запись вида [worker] захватывает текущее значение отдельно: если это ссылка на объект, замыкание получает собственную сильную ссылку на тот же объект, но последующее переназначение исходной переменной не меняет захваченное значение.
Сильная ссылка в общем хранилище может продлить жизнь объекта. Это не является циклом само по себе: цикл появляется только тогда, когда через цепочку ссылок объект в итоге владеет хранилищем или замыканием, которое владеет этим же объектом.
Общее захваченное состояние не обеспечивает автоматическую потокобезопасность. Одновременное чтение и изменение такой переменной из разных потоков или задач требует отдельной синхронизации; ARC управляет временем жизни ссылок, но не защищает доступ к данным от гонок.
Сервис запускает асинхронную операцию и возвращает два callback-замыкания: одно сообщает о текущем результате, другое отменяет операцию. Оба замыкания захватывают изменяемую переменную с объектом операции. После завершения экрана объект неожиданно остаётся в памяти, потому что замыкания или владелец callback-цепочки продолжают удерживать общее хранилище с сильной ссылкой.
Возможны такие решения:
nil и должен корректно обработать отсутствие владельца.Если callback должен работать только пока существует экран, разумно хранить состояние операции отдельно, а ссылку из callback на экран делать слабой. Если же callback обязан владеть неизменяемой конфигурацией операции, лучше использовать capture list и явно зафиксировать нужное значение. Такой выбор отделяет требуемое время жизни от случайного захвата изменяемой переменной.
Чем захват var отличается от захвата через capture list?
Захват var сохраняет общее хранилище, поэтому переназначение переменной видно всем замыканиям, которые используют это хранилище. Capture list вычисляется при создании замыкания и создаёт отдельные захваченные значения. Для ссылочного типа это обычно отдельная сильная ссылка на тот же объект, а не копирование самого объекта.
Когда освобождается объект, лежащий в захваченном хранилище?
При записи нового значения старая сильная ссылка освобождается в момент, когда операция присваивания заменяет значение в хранилище. Если объект не удерживается другими сильными ссылками, после этого он становится доступным для уничтожения. Если хранилище сохраняет старое значение или его удерживает другое замыкание, объект продолжит жить.
Почему наличие общего хранилища не означает retain cycle?
Общее хранилище само по себе лишь сохраняет захваченную переменную. Цикл возникает только при замкнутом графе владения: например, объект сильно владеет замыканием, замыкание сильно владеет хранилищем, а хранилище сильно владеет исходным объектом. Если одна из связей сделана слабой или цепочка разрывается после завершения операции, ARC сможет освободить объекты.