Вызов функции с аргументом типа, реализующего Copy: почему после вызова исходное имя остаётся доступным?

Вызов функции с аргументом типа, реализующего Copy: почему после вызова исходное имя остаётся доступным?

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

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

Если тип аргумента реализует Copy, передача значения по значению создаёт его неявную копию, а не перемещает исходное значение. Поэтому исходное имя остаётся доступным после вызова функции. Это работает только для типов, чьи значения безопасно копировать побитово и которые не реализуют Drop.

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

Модель владения Rust должна отличать простое дублирование небольшого значения от передачи владения ресурсом. Для String, файлового дескриптора или другого владеющего объекта неявная копия могла бы привести к двойному освобождению или неоднозначному владению.

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

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

При передаче значения по значению Rust обычно перемещает его в параметр функции. После перемещения прежнее имя нельзя использовать, поскольку владельцем значения становится параметр.

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

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

Copy — это маркерный трейт, который разрешает неявное копирование значения. При передаче Copy-значения в функцию исходное значение не инвалидируется: в параметр попадает отдельная копия.

#[derive(Copy, Clone)] struct UserId(u64); fn log_id(id: UserId) { println!("{}", id.0); } fn main() { let id = UserId(42); log_id(id); log_id(id); }

Здесь оба вызова корректны: id копируется при каждом вызове. Копируется само значение UserId, а не какой-либо внешний ресурс.

Тип может реализовать Copy, только если все его поля также реализуют Copy, а сам тип не имеет пользовательского поведения освобождения через Drop. Поэтому структуры с String, Vec<T> или другими владеющими полями обычно не могут быть Copy.

Copy не означает глубокое копирование. Например, копирование &T дублирует ссылку, но не значение T, на которое она указывает. Это безопасно для общей неизменяемой ссылки, однако &mut T не является Copy, поскольку неявное дублирование уникального доступа нарушило бы правила заимствования.

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

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

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

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

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

Выбранное решение — сделать тип идентификатора Copy и Clone. Тогда передача по значению не инвалидирует исходное имя, а сама семантика типа явно сообщает, что его безопасно неявно копировать. Для идентификатора с String-полем такой подход не подходит: там следует выбрать заимствование, перемещение или явный Clone в зависимости от задачи.

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

  1. Означает ли Copy, что Clone всегда выполняет глубокое копирование?

    Нет. Copy требует реализации Clone, но для простого типа clone() обычно эквивалентен копированию самого значения. Если тип содержит ссылку, клонируется ссылка, а не объект по ней. Глубокое копирование возможно только тогда, когда это предусмотрено реализацией Clone конкретного типа.

  2. Можно ли сделать Copy для структуры, содержащей Vec?

    Нет, автоматически это невозможно: Vec<T> не является Copy, поскольку владеет буфером в куче. Структура может реализовать Clone, чтобы явно дублировать содержимое вектора, но это не превращает её в Copy и не делает копирование неявным.

  3. Меняет ли Copy правила владения данными, на которые указывает значение?

    Нет. Copy меняет только поведение самого значения при операциях, где возможен move. Копирование &T создаёт ещё одну ссылку с теми же правилами времени жизни и заимствования; оно не копирует T и не продлевает его время жизни. Для владеющего указателя или контейнера наличие Copy было бы недопустимо именно потому, что простое побитовое дублирование нарушило бы уникальность владения.