В синхронном не изолированном коде вызывается синхронный метод, помеченный @MainActor. Перейдёт ли Swift автоматически на главный актор?
Нет. Синхронный вызов не выполняет автоматический переход на главный актор: метод либо должен вызываться из уже изолированного контекста, либо такой вызов будет отклонён проверкой конкурентности. Если изолирование обойдено через небезопасную или устаревшую границу, метод может выполниться в текущем контексте, поэтому одна лишь аннотация @MainActor не превращает синхронный вызов в безопасный переход.
Глобальные акторы появились как способ связать состояние и операции с определённым изолированным исполнителем. Наиболее известный пример — пользовательский интерфейс, который должен изменяться только из главного акторного контекста.
До структурированной конкурентности разработчики часто полагались на соглашение «этот метод вызывается на главном потоке». Такое соглашение легко нарушалось callback-ами, фоновыми очередями и сторонними API. Акторная изоляция переносит часть этой проверки в компилятор, но не может автоматически изменить семантику уже выполняющегося синхронного вызова без точки приостановки.
Синхронная функция не может приостановить текущий стек вызовов и дождаться, пока главный актор освободится. Поэтому у Swift нет обычного механизма, который незаметно поставил бы такой вызов в очередь и продолжил выполнение после перехода.
Если разрешить прямой вызов из произвольного синхронного контекста, код, изменяющий состояние интерфейса, может выполняться не в изолированном контексте. Последствия — гонки данных, некорректное состояние UI и ошибки, проявляющиеся только при определённом расписании потоков.
Синхронный метод, изолированный @MainActor, сохраняет синхронную природу. Вызов из другого изолированного контекста допустим только при соблюдении правил изоляции; вызов из не изолированного кода при строгой проверке конкурентности будет диагностирован компилятором.
Для асинхронного вызывающего кода переход выполняется через точку await. В этот момент задача может быть приостановлена, а продолжение — запланировано на главный актор. Сам метод при этом не становится «асинхронным» по объявлению: await требуется из-за возможного перехода между изолированными контекстами.
refresh корректно передаёт выполнение главному актору. legacyCallback создаёт отдельную задачу, явно привязанную к главному актору; это безопаснее прямого вызова, но задача выполняется независимо от синхронной функции, поэтому её завершение и отмену нужно проектировать отдельно.
Варианты имеют разные компромиссы:
async — лучший вариант для структурированного управления жизненным циклом и ошибок;Task { @MainActor in ... } — практичный адаптер для callback API, но это уже отдельная незавершённая задача;Важно отличать изоляцию от физического потока. В корректном асинхронном переходе главный актор выбирает подходящий исполнитель, обычно связанный с главным потоком, но гарантия языка формулируется через акторную изоляцию, а не через ручную проверку номера потока.
Сетевой клиент получает результат через старый синхронный callback и пытается напрямую вызвать синхронный метод модели экрана. В строгом режиме Swift такой вызов не проходит проверку, поскольку callback не доказал принадлежность к главному актору.
Можно было бы обернуть тело в отдельную задачу без указания актора. Это не даёт нужной гарантии: обычная задача наследует часть контекста создания, но в не изолированном callback-е нельзя рассчитывать на требуемую изоляцию.
Другой вариант — использовать Task { @MainActor in ... }. Он явно задаёт целевой контекст и хорошо подходит для короткого обновления UI, но результат ошибки и отмена сетевой операции не будут автоматически связаны с этой задачей.
Предпочтительное решение — сделать слой адаптации асинхронным: завершить callback через continuation, дождаться результата в родительской задаче, а затем вызвать await-метод модели. Так сохраняются структурированный жизненный цикл, распространение отмены и явная граница перехода на главный актор.
@MainActor синхронный метод в асинхронный?Нет. Аннотация задаёт изоляцию, но не добавляет метку async и не меняет тело метода. Однако вызов из другого изолированного контекста может потребовать await, потому что переход к главному актору потенциально приостанавливает задачу.
Если метод вызывается уже внутри @MainActor-контекста, переход не нужен: код продолжает выполняться в той же изоляции. Нельзя делать вывод, что любой синхронный метод с @MainActor можно безопасно вызвать откуда угодно.
Task.detached?Task.detached не наследует акторный контекст вызывающей задачи. Поэтому прямой вызов метода, изолированного главным актором, нарушает правила изоляции и при строгой проверке должен быть отклонён компилятором.
Корректный вариант — выполнить вызов через асинхронный переход или создать задачу с явным главным актором. Само нахождение внутри Task.detached не даёт права обращаться к главному актору синхронно.
Task { @MainActor in ... } безопаснее прямого вызова, но не всегда является лучшим решением?Такая конструкция не вызывает тело немедленно в текущем стеке. Она создаёт задачу, изолированную главным актором, и планировщик выполнит её в подходящем контексте.
Недостаток — отделение жизненного цикла: вызывающая функция может завершиться раньше, задача может быть отменена независимо, а ошибка не попадёт автоматически обратно в вызывающий код. Поэтому для одноразового UI-обновления это удобный адаптер, а для последовательности зависимых операций предпочтительнее передать результат через async/await и сохранить структурированную связь задач.