Avatar ·

Как сделать тип только перемещаемым (move), но не копируемым (Copy) в Rust?

Всем привет! Я начал изучать Rust недавно и столкнулся с концепцией владения (ownership) и типажами Copy и Clone. Вроде бы я понял, что примитивные типы, такие как i32 или bool, реализуют Copy, поэтому при присваивании они копируются, а оригинал остаётся доступным. А более сложные типы, например String, — только Clone, и при присваивании они перемещаются (move). Но у меня возник вопрос: могу ли я сам создать свой собственный тип (например, структуру), который будет вести себя как String — то есть будет только перемещаемым, и его нельзя будет случайно скопировать? Например, я хочу сделать тип, который представляет уникальный идентификатор или какой-то ресурс, который не должен дублироваться. Если я просто объявлю структуру и добавлю derive(Clone, Copy), то она будет копироваться при каждом присваивании. А если не добавлять эти derive, что произойдёт? Вот пример кода, который меня смущает: ```rust struct MyType { value: i32, } fn main() { let a = MyType { value: 10 }; let b = a; // Что происходит здесь? Перемещение или копирование? println!("{}", a.value); // Можно ли это сделать после присваивания? } ``` Я прочитал, что по умолчанию в Rust все типы перемещаются, если они не реализуют Copy. Но если я не добавляю derive(Copy), то мой тип будет только перемещаемым? Или есть какие-то подводные камни? Объясните, пожалуйста, как правильно создать тип, который гарантированно нельзя скопировать, только переместить. Заранее спасибо! <<>>

Краткий ответ: да, тип по умолчанию является только перемещаемым

В Rust любой тип, который не реализует типаж Copy, автоматически становится только перемещаемым (move-only). Это фундаментальное правило: если тип не помечен как Copy, при присваивании или передаче в функцию происходит перемещение (move), а не копирование. Исходная переменная становится недействительной.

Ваш пример: почему это работает

В вашем коде структура MyType не реализует Copy (и даже Clone). Поэтому строка let b = a; — это перемещение. После неё a больше не владеет данными, и попытка использовать a.value приведёт к ошибке компиляции:

error[E0382]: borrow of moved value: `a.value`
 --> src/main.rs:7:28
  |
6 |     let b = a;
  |         - value moved here
7 |     println!("{}", a.value);
  |                    ^^^^^^^ value borrowed here after move

Как явно запретить копирование

Если вы хотите быть уверены, что ваш тип никогда не будет скопирован (даже если кто-то позже попытается добавить derive(Copy)), вы можете использовать один из двух подходов:

  1. Стандартный способ — просто не реализуйте Copy. Это уже даёт поведение move-only. Но если в структуре есть поля, которые сами реализуют Copy (как i32), компилятор может разрешить добавить Copy к вашей структуре. Чтобы предотвратить это даже случайно, используйте второй подход.
  2. Добавьте поле, которое не реализует Copy. Например, включите PhantomData или тип-заглушку:
use std::marker::PhantomData;

struct UniqueResource {
    id: i32,
    // Это поле не имеет размера, но делает тип не-Copy
    _not_copy: PhantomData<*const ()>,
}

fn main() {
    let a = UniqueResource { id: 42, _not_copy: PhantomData };
    let b = a; // move
    // println!("{}", a.id); // Ошибка! a больше недействителен
}

Почему это работает?

PhantomData<*const ()> — это тип, который не реализует Copy (потому что сырые указатели не являются копируемыми по умолчанию). Компилятор видит, что структура содержит такое поле, и автоматически запрещает реализацию Copy для всей структуры. Даже если вы попытаетесь написать #[derive(Copy)] — компилятор выдаст ошибку.

Важные нюансы

  • Типаж Clone не делает тип копируемым. Clone — это явное клонирование через метод .clone(). Вы можете реализовать Clone для move-only типа, и это не нарушит правила перемещения. Например:
#[derive(Clone)]
struct MyType {
    value: i32,
    // без Copy — только move
}

fn main() {
    let a = MyType { value: 10 };
    let b = a.clone(); // явное копирование — разрешено
    let c = a;         // перемещение — разрешено, но a теперь недействителен
}
  • Типаж Copy — это маркер. Он говорит компилятору: «Этот тип можно безопасно копировать побитово». Если вы его не реализуете, компилятор будет требовать явного перемещения или клонирования.

Проверка на практике

Вы можете проверить, реализует ли ваш тип Copy, с помощью вспомогательной функции:

fn is_copy<T: Copy>() {}
fn main() {
    // is_copy::<MyType>(); // Ошибка, если MyType не Copy
}

Итог

Чтобы создать тип, который только перемещается и не копируется:

  1. Не добавляйте derive(Copy).
  2. Убедитесь, что все поля типа сами не реализуют Copy (или используйте PhantomData для защиты).
  3. При желании добавьте derive(Clone) для явного клонирования.

Это даст вам поведение, аналогичное String или Vec: при присваивании — перемещение, при явном вызове .clone() — копирование.

<<>>

Альтернативный подход: использование типажа Drop для гарантии

Если вы хотите быть абсолютно уверены, что ваш тип никогда не будет скопирован (даже случайно), вы можете реализовать для него типаж Drop. Дело в том, что Rust запрещает одновременно реализовывать Copy и Drop для одного типа. Это логично: если у типа есть деструктор (Drop), то его нельзя просто так скопировать побитово, иначе деструктор может быть вызван дважды для одних и тех же данных.

struct UniqueId {
    id: i32,
}

impl Drop for UniqueId {
    fn drop(&mut self) {
        println!("Уникальный ID {} уничтожен", self.id);
    }
}

fn main() {
    let a = UniqueId { id: 1 };
    let b = a; // move — OK
    // let c = a; // Ошибка! a перемещён
    // Если попытаться добавить #[derive(Copy)] — ошибка компиляции
}

Этот метод особенно полезен для типов, управляющих ресурсами (файлы, сокеты, выделенная память), где двойное освобождение привело бы к катастрофе. Drop не только блокирует Copy, но и гарантирует выполнение очистки при завершении жизни объекта.

Недостаток: если ваш тип не требует деструктора, добавление Drop может быть избыточным. В таком случае лучше использовать PhantomData из первого ответа — он не добавляет накладных расходов и не меняет семантику.