Программирование SwiftARC и памятьSenior-разработчик iOS на Swift

Меняет ли аннотация @Sendable способ владения захваченным экземпляром класса?

Меняет ли аннотация @Sendable способ владения захваченным экземпляром класса?

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

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

Нет. @Sendable задаёт требования конкурентной безопасности замыкания, но не меняет семантику владения: сильный захват продолжает удерживать экземпляр, а weak и unowned сохраняют свои обычные правила времени жизни.

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

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

До появления структурированной конкурентности основная задача ARC заключалась в автоматическом управлении временем жизни объектов внутри обычной модели ссылок. С появлением async/await, задач и изоляции исполнителей возникла отдельная проблема: безопасно ли передавать захваченные значения между потоками и конкурентными контекстами.

Для этого в Swift появился протокол Sendable и аннотация @Sendable. Они предотвращают небезопасную передачу изменяемого состояния, но не заменяют ARC и не превращают сильные ссылки в слабые.

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

Замыкание может жить дольше функции, в которой оно создано. Если оно сильно захватывает экземпляр класса, этот экземпляр будет удерживаться хранилищем замыкания до освобождения самого замыкания.

Добавление @Sendable не разрывает такое владение. Поэтому ошибочно считать, что передача callback в Task или другой конкурентный контекст автоматически предотвращает retain cycle либо сокращает время жизни объекта.

Обратная ошибка тоже опасна: использование weak только ради прохождения проверки конкурентности не делает доступ к объекту безопасным. Объект может исчезнуть до выполнения callback, а само состояние класса всё ещё может быть небезопасным для конкурентного доступа.

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

ARC учитывает операции владения, возникающие из захватов замыкания. Обычный захват экземпляра является сильным, если явно не указано иное. Такой захват увеличивает время жизни объекта независимо от того, имеет ли тип замыкания аннотацию @Sendable.

@Sendable проверяет, какие данные замыкание переносит между конкурентными контекстами. Значение захваченного типа должно быть безопасно передаваемо согласно правилам Swift: например, это может быть значение-тип с Sendable, актор или экземпляр класса с явно обеспеченной потокобезопасностью.

weak и unowned меняют именно владение и поведение при уничтожении объекта. weak не удерживает объект и обнуляется, тогда как unowned не удерживает объект и приводит к ошибке при обращении после его уничтожения. Ни одна из этих ссылок сама по себе не гарантирует потокобезопасность.

Пометка @unchecked Sendable отключает часть статической проверки и перекладывает ответственность на разработчика. Она не меняет ARC: сильные свойства и сильные захваты по-прежнему удерживают экземпляры, а ошибки синхронизации могут привести к гонкам данных.

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

Экран запускает отложенную операцию, callback которой передаётся в конкурентную задачу. Разработчик добавляет @Sendable и ожидает, что контроллер перестанет удерживаться задачей. В результате экран всё ещё не освобождается, потому что callback сильно захватывает контроллер.

Возможны три решения:

  • оставить сильный захват — просто и безопасно для времени жизни, но задача может продлить жизнь экрана дольше требуемого;
  • заменить захват на weak — цикл и лишнее удержание устраняются, но callback должен корректно обработать отсутствие экрана;
  • изолировать состояние через актор или @MainActor — повышает безопасность доступа, но требует правильно организовать границы взаимодействия.

Обычно для UI выбирают слабый захват вместе с явной проверкой существования экрана, а изменяемое состояние передают через изолированный объект. Это отдельно решает две задачи: время жизни контроллера регулирует ARC, а безопасность доступа обеспечивает модель конкурентности.

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

  1. Гарантирует ли @Sendable, что замыкание не образует retain cycle?

Нет. @Sendable не анализирует граф владения как средство борьбы с циклами. Замыкание может сильно захватывать объект, объект — хранить это замыкание, и цикл останется. Для разрыва цикла нужны правильная архитектура владения, weak, unowned или явное освобождение callback.

  1. Достаточно ли слабого захвата, чтобы сделать доступ к классу безопасным между потоками?

Нет. Слабая ссылка только не удерживает объект и может быть обнулена. Она не синхронизирует чтение и изменение его свойств, не устраняет гонки и не гарантирует, что объект останется жив после отдельного чтения weak-ссылки.

  1. Что произойдёт, если класс объявлен @unchecked Sendable, но его сильное состояние изменяется из разных задач?

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