Почему я не могу переместить общий ссылочный тип на владимый вектор, который впоследствии не используется?

Вопрос или проблема

Мне интересно, почему невозможно переместить значение из разделяемой ссылки на вектор, если вектор принадлежит и не используется после этого?

У меня есть код, который выглядит следующим образом:

#[derive(Debug)]
struct Item(usize);

fn main() {
    let items = vec!(Item(1), Item(2), Item(3), Item(4));
    let num: Item = handle_selection_and_return(items);
    dbg!(num);
}

/// Элемент выбирается здесь, например, через интерфейс пользователя.
fn select(items: &[Item]) -> &Item {
    return &items[1];
}

fn handle_selection_and_return(items: Vec<Item>) -> Item {
    // Дополнительный код, который может добавить дополнительные элементы.
    // ...

    // Теперь выберите элемент и верните его.
    *select(&items)
}

Это приведет к следующей ошибке:

error[E0507]: cannot move out of a shared reference
  --> src/main.rs:20:5
   |
20 |     *select(&items)
   |     ^^^^^^^^^^^^^^^ move occurs because value has type `Item`, which does not implement the `Copy` trait
   |
note: if `Item` implemented `Clone`, you could clone the value
  --> src/main.rs:2:1
   |
2  | struct Item(usize);
   | ^^^^^^^^^^^ consider implementing `Clone` for this type
...
20 |     *select(&items)
   |     --------------- you could clone this value

Для получения дополнительной информации об этой ошибке попробуйте `rustc --explain E0507`.
error: could not compile `playground` (bin "playground") due to 1 previous error

Ссылка на песочницу: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=4f7fdec2bede11ddc81031412aef26f4

Я не вижу, почему элемент нужно клонировать здесь, потому что вектор не используется после этого. Есть ли способ избежать ненужного клонирования?

Существует, по крайней мере, несколько разных способов взглянуть на это:

  • Что это будет значить для дизайна языка
  • Что это будет значить для поведения времени выполнения

Дизайн языка

Во-первых, с точки зрения семантики языка. То, что вы по сути спрашиваете, это что-то вроде: “если у меня есть функция, которая принимает ссылку и возвращает ссылку, почему Rust автоматически не преобразует это в получение и возвращение принадлежащего значения, когда точка вызова этой функции передает ссылку на объект, который ей принадлежит и собирается сбросить”.

Даже если язык мог бы это сделать (см. ниже), действительно ли это стоит добавленной сложности? Языки с неявными действиями могут быстро стать очень трудными для понимания: каждое действие само по себе звучит здорово, когда вы читаете об этом в документации, но затем вы видите строку, которая объединяет два или три из них весьма точным образом, и это может быстро привести к “Я на самом деле не понимаю, что делает этот код.”

Людям гораздо легче понять, если вы создадите select_owned(Vec<Item>) -> Item и просто будете использовать это.

Поведение времени выполнения: где живет Item?

Во-вторых, с точки зрения памяти: элементы в Vec живут в куче, а локальный Item живет в стеке. Когда вы сбрасываете Vec, его память в куче будет освобождена; что бы вы ожидали, что произойдет с этим значением Item в стеке, если оно каким-то образом было “перенаправлено” на (больше не действующий) элемент, выделенный в куче?

Как только Vec будет сброшен, его элементы больше не существуют. Если вы хотите продолжать использовать один элемент после его жизненного цикла, вам нужно будет скопировать его в новое место (в данном случае, в стек).

Ваши два стандартных варианта для этого: .clone() или копирование, последнее из которых вы пытаетесь сделать — но это требует, чтобы тип реализовывал Copy, что он не делает. (Обратите внимание, что в вашем примере вы можете легко добавить производный Copy; я не знаю, так ли это в вашем реальном коде.)

Ответ или решение

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

Проектирование языка

Во-первых, с точки зрения семантики языка, ваша проблема связана с тем, что Rust придерживается строгих правил владения и заимствования. Когда вы передаете ссылку на вектор (например, &items), вы говорите компилятору, что не планируете владеть этим вектором, а просто будете получать доступ к его элементам. В языке Rust не предусмотрена возможность автоматического "повышения" ссылки до владения, даже если вектор передается только для получения одного элемента, и лишь временно. Это решение позволяет избежать множества сложностей и недоразумений в коде.

Если бы Rust позволял автоматически поднимать ссылки до владения в таких ситуациях, это, безусловно, усложнило бы понимание кода. Код, который может неожиданно изменять свое поведение в зависимости от контекста заимствования, становится труднее читать и поддерживать. Поэтому, чтобы избежать таких ситуаций, Rust требует, чтобы код был максимально явным: вам нужно либо возвращать значение, либо явно осуществить копирование/перемещение.

Поведение во время выполнения: где живет Item?

Со второй точки зрения – с точки зрения управления памятью, объекты типа Item хранятся в куче в рамках вектора (Vec<Item>). Когда вы объявляете переменную типа Item, она располагается на стеке. Если в момент закрытия функции handle_selection_and_return вектор будет сброшен (дропнут), то выделенная в куче память для элементов Item будет освобождена, и использование таких значений станет невалидным.

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

Решение вашей проблемы

Если вы хотите избежать ошибки, у вас есть несколько вариантов:

  1. Реализовать трейт Clone: Если ваш тип Item будет реализовать трейт Clone, вы сможете вызвать метод .clone() на выбранном элементе.

    #[derive(Debug, Clone)]
    struct Item(usize);

    И затем в функции handle_selection_and_return просто использовать select(&items).clone().

  2. Изменить сигнатуру select: Вы можете изменить функцию select на возврат владения, принимая вектор, а не ссылку на срез:

    fn select(items: Vec<Item>) -> Item {
       items[1]
    }

    Это позволит вам избежать проблем с зависимостями между временем жизни.

  3. Использовать массив фиксированного размера или другой подход к хранению: В зависимости от контекста вашей задачи, возможно, вам стоит рассмотреть другие структуры данных, такие как массивы фиксированного размера, которые могут обеспечить безопасное владение элементами.

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

Оцените материал
Добавить комментарий

Капча загружается...