Разрешено ли объявить weak свойство типа замыкания в Swift, чтобы разорвать цикл с его владельцем?

Разрешено ли объявить weak-свойство типа замыкания в Swift, чтобы разорвать цикл с его владельцем?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Нет. В Swift weak применим только к экземплярам классов и class-bound протоколам, а тип замыкания не удовлетворяет этому ограничению. Чтобы разорвать цикл, обычно делают слабым захват владельца внутри замыкания, а не саму ссылку на замыкание.

Исторический контекст

ARC автоматизирует подсчёт сильных ссылок на экземпляры классов, но не определяет семантику владения для любых ссылочных по реализации значений. Поэтому Swift явно разделяет обычные ссылочные значения, которыми можно владеть, и специальные слабые ссылки на классы, которые runtime умеет обнулять при уничтожении объекта.

Слабая ссылка появилась как средство описать отношение «наблюдаемый объект не владеет владельцем». Для замыканий аналогичная проблема решается через capture list, потому что замыкание само может захватывать объекты.

Постановка проблемы

Замыкание часто хранится в свойстве объекта, например как callback. Если это замыкание сильно захватывает тот же объект, возникает цикл: объект владеет замыканием, а замыкание владеет объектом.

Попытка объявить само свойство-замыкание слабым не является допустимым решением: компилятор отклонит такое объявление. Кроме того, даже гипотетически слабая ссылка на callback не объяснила бы, как именно замыкание должно владеть захваченным объектом.

Подробное решение

Тип замыкания является ссылочным значением, но это не делает его допустимым типом для weak. Ограничение weak связано не с общей ссылочной природой значения, а с тем, что оно должно представлять экземпляр класса или class-bound протокола, для которого runtime поддерживает обнуление слабых ссылок.

Цикл разрывают ослаблением захвата объекта внутри замыкания:

final class Controller { var callback: (() -> Void)? func bind() { callback = { [weak self] in self?.refresh() } } func refresh() {} }

Здесь Controller сильно владеет замыканием через callback, но замыкание хранит self слабо. После исчезновения остальных сильных ссылок Controller может быть уничтожен, а слабая ссылка внутри замыкания автоматически станет nil.

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

Ситуация из практики

Экран хранит callback для обработки события, а callback вызывает метод этого же экрана. Вариант с сильным захватом прост, но удерживает экран и его callback друг через друга; deinit экрана не вызывается после закрытия.

Вариант с попыткой сделать свойство callback слабым невозможен: тип замыкания не поддерживает weak. Вариант с [unowned self] устраняет цикл, но безопасен только при строгой гарантии, что callback не будет вызван после уничтожения экрана.

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

Что кандидаты часто упускают

1. Почему замыкание считается ссылочным значением, но всё равно не поддерживает weak?

Ссылочная семантика и допустимость weak — разные свойства. Замыкание может храниться и копироваться как ссылочное значение, однако weak в Swift ограничен экземплярами классов и class-bound протоколами. Для захвата объектов замыкание использует собственные правила capture list.

2. Что именно нужно сделать слабым при цикле между объектом и callback?

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

3. Может ли callback с [weak self] всё равно временно удерживать объект?

Да. При чтении слабой ссылки и последующем создании сильной локальной ссылки объект может оставаться живым до конца текущего использования. Это нормальная временная гарантия выполнения операции, но она не превращает слабый захват в постоянное владение и не создаёт цикл сама по себе.