Чи можна зробити тип лише рухомим, а не копіюваним?


96

Примітка редактора : це питання було задано до Руст 1.0, і деякі твердження у питанні не обов'язково відповідають істині в Руст 1.0. Деякі відповіді було оновлено для розгляду обох версій.

У мене є така структура

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

Якщо я передаю це функції, вона неявно копіюється. Зараз я іноді читаю, що деякі значення не можна скопіювати і тому їх потрібно перемістити.

Чи можна було б зробити цю структуру Tripletнеможливо скопіювати? Наприклад, чи можна було б застосувати рису, яка зробила б Tripletкопіювану, а отже, «рухомою»?

Я десь читав, що потрібно застосовувати Cloneрису, щоб копіювати речі, які не можна неявно копіювати, але я ніколи не читав про навпаки, тобто наявність чогось, що неявно копіюється, і робить його некопіювальним, щоб він рухався натомість.

Це взагалі має сенс?


1
paulkoerbitz.de/posts/… . Хороші пояснення тут, чому рухатись проти копії.
Шон Перрі,

Відповіді:


164

Передмова : Цей відповідь була написана до неавтоматичного вбудованих ознак -specifically на Copyаспектах ставилися реалізовані. Я використовував блок-лапки, щоб вказати розділи, які застосовувались лише до старої схеми (тієї, що застосовувалася, коли було задано питання).


Старий : Щоб відповісти на основне запитання, можна додати поле маркера, що зберігає NoCopyзначення . Напр

struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}

Ви також можете зробити це, використовуючи деструктор (за допомогою реалізації Dropознаки ), але використання типів маркерів є кращим, якщо деструктор нічого не робить.

Типи тепер переміщуються за замовчуванням, тобто коли ви визначаєте новий тип, який він не реалізує, Copyякщо ви явно не реалізуєте його для свого типу:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move

Реалізація може існувати лише за умови, що кожен тип, що міститься в новому, structабо enumє самим собою Copy. Якщо ні, компілятор надрукує повідомлення про помилку. Він також може існувати, лише якщо тип не має Dropреалізації.


Щоб відповісти на запитання, яке ви не задавали ... "що з ходами та копію?":

Спочатку я визначу дві різні "копії":

  • байт копія , яка просто неглибоко копіювання об'єкта байт в байт, а не слід покажчики, наприклад , якщо у вас є (&usize, u64), це 16 байт на 64-розрядному комп'ютері, і неповну копію буде приймати ці 16 байт і тиражування їх значення в якомусь іншому 16-байтовому фрагменті пам'яті, не торкаючись значка usizeна іншому кінці &. Тобто це еквівалентно дзвінку memcpy.
  • семантична копія , дублюючи значення для створення нового (кілька) незалежного примірника , який можна безпечно використовувати окремо для старої. Наприклад, семантична копія Rc<T>включає просто збільшення кількості посилань, а семантична копія Vec<T>включає створення нового розподілу, а потім семантичну копію кожного збереженого елемента зі старого в новий. Це можуть бути глибокі копії (наприклад Vec<T>) або неглибокі (наприклад Rc<T>, не стосуються збережених T), Cloneвільно визначається як найменший обсяг роботи, необхідний для семантичного копіювання значення типу Tзсередини &Tдо T.

Rust - це як C, кожне використання значення за значенням - це байтова копія:

let x: T = ...;
let y: T = x; // byte copy

fn foo(z: T) -> T {
    return z // byte copy
}

foo(y) // byte copy

Вони являють собою байтові копії, незалежно від Tпереміщення чи ні, або "неявно копіюються". (Щоб бути зрозумілим, вони не обов’язково буквально побайтові копії під час виконання: компілятор може вільно оптимізувати копії, якщо поведінка коду збережена.)

Однак є основна проблема з байтовими копіями: ви отримуєте дубльовані значення в пам'яті, що може бути дуже погано, якщо вони мають деструктори, наприклад

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here

Якби це wбула просто байтова копія, vтоді було б два вектори, що вказували б на одне і те ж розподіл, обидва з деструкторами, які його звільняють ... викликаючи подвійний вільний , що є проблемою. Примітка. Це було б чудово, якби ми зробили семантичну копію vв w, оскільки тоді це wбуло б самостійно, Vec<u8>і деструктори не топтали б один одного.

Тут є кілька можливих виправлень:

  • Нехай програміст обробляє це, як C. (у C немає деструкторів, тому це не так погано ... замість цього ви просто залишаєтесь із витоками пам'яті.: P)
  • Виконайте семантичну копію неявно, так що wвона має власне виділення, як C ++ з її конструкторами копій.
  • Вважайте використання за вартістю як передачу права власності, так що воно vбільше не може використовуватися і не запускається його деструктор.

Останнє - те, що робить Rust: переміщення - це просто використання за значенням, де джерело статично недійсним, тому компілятор запобігає подальшому використанню недійсної пам’яті.

let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value

Типи, які мають деструктори, повинні переміщатися, коли використовується побічне значення (інакше, коли копіюється байт), оскільки вони мають управління / право власності на деякий ресурс (наприклад, виділення пам'яті або дескриптор файлу), і дуже малоймовірно, що байтова копія буде правильно дублювати це право власності.

"Ну ... що таке неявна копія?"

Подумайте про примітивний тип, наприклад u8: байтова копія проста, просто скопіюйте один байт, а семантична копія так само проста, скопіюйте єдиний байт. Зокрема, байтова копія - це семантична копія ... Rust навіть має вбудовану ознаку,Copy яка фіксує, які типи мають однакові семантичні та байтові копії.

Отже, для цих Copyтипів використання за значенням також є автоматично семантичними копіями, і тому цілком безпечно продовжувати використовувати джерело.

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine

Старий : NoCopyмаркер замінює автоматичну поведінку компілятора, припускаючи, що типами, які можуть бути Copy(тобто містять лише сукупності примітивів та &), є Copy. Однак це зміниться, коли буде застосовано вбудовані риси .

Як уже згадувалося вище, вбудовані властивості, що вмикаються, реалізовані, тому компілятор більше не має автоматичної поведінки. Однак правило, яке використовувалось для автоматичної поведінки в минулому, є однаковими правилами перевірки законності впровадження Copy.


@dbaupp: Чи могли б ви випадково знати, в якій версії Rust з'явилися вбудовані риси, що дозволяють? Я б подумав 0.10.
Matthieu M.

@MatthieuM. він ще не впроваджений, і насправді нещодавно було запропоновано деякі зміни щодо дизайну вбудованих компонентів .
huon

Я думаю, що стару цитату слід стерти.
Stargateur

1
# [вивести (Копіювати, Клонувати)] слід використовувати на
Triplet

6

Найпростіший спосіб - це вбудувати у свій тип щось, що не можна скопіювати.

Стандартна бібліотека забезпечує "тип маркера" саме для цього випадку використання: NoCopy . Наприклад:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}

15
Це невірно для Rust> = 1,0.
malbarbo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.