В API Swift параметр помечен sending: какую гарантию должен обеспечить вызывающий код после передачи значения?

В API Swift параметр помечен sending: какую гарантию должен обеспечить вызывающий код после передачи значения?

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

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

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

 sending — это статический контракт передачи, а не копирование, блокировка или автоматическое устранение гонок. Вызываемая сторона получает право использовать значение независимо от исходного контекста, а ответственность за отсутствие последующего конкурентного доступа переносится на вызывающий код.

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

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

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

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

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

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

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

Параметр sending сообщает компилятору, что значение может пересечь границу конкурентности с передачей владения. После такого вызова прежний контекст не должен продолжать использовать переданное значение или его несамопередаваемые алиасы. Компилятор проверяет это правило на этапе компиляции и выдаёт ошибку либо предупреждение при потенциально опасном последующем доступе.

Механизм не меняет объект во время выполнения: ссылочный объект не копируется, а параметр не становится автоматически Sendable. Безопасность достигается тем, что после передачи исключается параллельное использование старым владельцем.

final class Packet { var bytes = [UInt8]() } actor Receiver { func accept(_ packet: sending Packet) { packet.bytes.append(1) } } func deliver(_ packet: sending Packet, to receiver: Receiver) async { await receiver.accept(packet) }

В примере Packet может оставаться обычным изменяемым ссылочным типом. После передачи через sending вызывающий код не должен обращаться к packet; actor становится единственным контекстом, который продолжает его использовать.

Важно отличать передачу от совместного доступа. Если значение нужно использовать и в отправителе, и в получателе, sending не является решением: потребуется Sendable, неизменяемость, actor, блокировка или другая синхронизация. Также sending не защищает от ошибок, если разработчик сознательно отключает проверки через небезопасные конструкции.

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

Сервис загрузки получает большой изменяемый буфер и передаёт его actor, отвечающему за запись на диск. Рассматривались три варианта: сделать буфер @unchecked Sendable, копировать его перед передачей или использовать sending.

@unchecked Sendable сохранил бы возможность совместного доступа, но потребовал бы самостоятельно доказывать отсутствие гонок. Копирование было бы проще для рассуждения, однако увеличило бы потребление памяти и задержку. sending выбран, поскольку после передачи исходный код больше не нуждается в буфере: объект передаётся без копирования, а компилятор контролирует отсутствие дальнейшего доступа.

В результате граница владения стала явной, потенциальная гонка была исключена статически, а лишняя синхронизация и копирование не потребовались. Если позже появится необходимость повторно использовать буфер, контракт придётся изменить, например перейти к передаче Sendable-значения или обеспечить синхронизированный общий доступ.

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

  1. Делает ли sending тип Sendable?

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

  1. Гарантирует ли sending копирование значения?

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

  1. Можно ли после вызова всё-таки использовать исходную переменную, если фактически вызываемая функция не сохраняет аргумент?

Нельзя полагаться на реализацию функции. Контракт sending допускает передачу значения в другой конкурентный контекст, поэтому проверка выполняется по границе API, а не по анализу того, используется ли аргумент внутри конкретного тела. Если значение должно остаться доступным отправителю, следует выбрать контракт совместного безопасного доступа, а не sending.