Что удерживает escaping-замыкание, если оно захватывает локальную weak-ссылку без явного списка захвата?
Escaping-замыкание удерживает не сам объект, а хранилище локальной переменной, в котором находится weak-ссылка. Поэтому объект не получает дополнительного сильного владельца: после исчезновения последней сильной ссылки weak-ссылка автоматически станет nil.
При этом захват хранилища сохраняет доступ к самой переменной после выхода из функции. Это не меняет семантику weak и не превращает ссылку в сильную.
ARC автоматизировал управление временем жизни классовых объектов через подсчёт сильных ссылок. Для обратных или необязательных связей появились weak-ссылки, которые не владеют объектом и автоматически обнуляются при его уничтожении.
Замыкания потребовали отдельного правила: локальные переменные могут переживать функцию, если их захватывает escaping-замыкание. Поэтому Swift сохраняет для замыкания специальное хранилище захваченной переменной, а не саму переменную на стеке.
Важно отличать два уровня: замыкание может владеть хранилищем переменной, но это не означает, что оно владеет объектом, на который указывает переменная. Ошибка в этом различии приводит к неверному выводу, будто захват локальной weak-ссылки защищает объект от уничтожения.
Если объект уничтожен до выполнения замыкания, чтение ссылки даст nil. Если же переменная была объявлена как обычная сильная ссылка, захват её хранилища приведёт к сохранению сильной ссылки и может продлить жизнь объекта или создать цикл.
При захвате изменяемой локальной переменной Swift помещает её значение в общее закрытое хранилище. Escaping-замыкание сохраняет это хранилище после возврата функции, чтобы переменная оставалась доступной.
Для weak-переменной само хранилище содержит специальную слабую ссылку. ARC отслеживает объект через инфраструктуру weak-ссылок и при уничтожении объекта обнуляет это хранилище. Замыкание продолжает существовать, но при следующем чтении получает nil.
В примере сильная ссылка на Service исчезает при выходе из makeCallback. Захваченное замыканием хранилище остаётся жить, но объект уже уничтожен, поэтому weak-ссылка равна nil.
Есть существенное отличие от явного списка захвата. Запись с [weak object] обычно создаёт отдельную слабую захваченную ссылку на момент формирования замыкания, тогда как захват локальной переменной без списка захвата сохраняет общее хранилище этой переменной. Если переменную позже переназначить, замыкание увидит новое значение из того же хранилища.
Такое замыкание не создаёт retain cycle через сам объект. Однако цикл всё ещё возможен по другой причине: например, если объект сильно владеет замыканием, а замыкание захватывает отдельный объект сильной ссылкой.
Метод создаёт callback и возвращает его вызывающему коду. Внутри метода сервис хранится в локальной weak-переменной, потому что callback не должен владеть сервисом.
Вариант с обычной локальной переменной проще, но замыкание будет сильно удерживать её значение после выхода из метода. Это может быть правильным решением для обязательной зависимости, но ошибочным для делегата, контроллера или временного сервиса.
Вариант с [weak service] явно фиксирует слабую семантику и обычно лучше документирует намерение, но захватывает ссылку отдельно. Захват локальной weak-переменной без списка сохраняет общее хранилище и удобен, когда callback должен видеть последующие переназначения этой переменной.
Выбор зависит от требуемого поведения: если callback должен наблюдать текущее значение переменной, подходит захват хранилища; если нужна независимая слабая ссылка на конкретное значение в момент создания callback, следует использовать явный список захвата. В обоих случаях объект не удерживается callback сильной ссылкой.
Дополнительный вопрос 1: Превращает ли сохранение хранилища локальной переменной weak-ссылку в сильную?
Нет. Хранилище и объект — разные сущности. Замыкание удерживает хранилище, но weak-ячейка внутри него не увеличивает число сильных ссылок на объект и обнуляется при его уничтожении.
Дополнительный вопрос 2: Что увидит замыкание, если локальную weak-переменную переназначить после его создания?
При захвате переменной без явного списка захвата замыкание использует общее хранилище. Поэтому оно увидит последнее значение, записанное в эту переменную, либо nil, если ссылка была очищена или объект уже уничтожен.
Дополнительный вопрос 3: Может ли объект исчезнуть между проверкой weak-ссылки и следующим использованием?
Да, если между операциями ссылка читается несколько раз или доступ происходит при конкурентном изменении владения. Одно чтение weak-ссылки обычно даёт временное сильное значение на время использования этого результата, но сама weak-ссылка не является средством синхронизации между потоками. Для согласованного доступа нужны подходящие средства синхронизации или изоляции.