Измените вариант перечисления при перемещении поля в новый вариант

Я хочу обновить перечислимый вариант при перемещении поля старого варианта на новое без клонирования:

enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: &mut X) {
    *x = match *x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    }
}

Это не работает, потому что мы не можем переместить s из &mut X:

error[E0507]: cannot move out of borrowed content
 --> src/lib.rs:7:16
  |
7 |     *x = match *x {
  |                ^^
  |                |
  |                cannot move out of borrowed content
  |                help: consider removing the '*': 'x'
8 |         X::X1(s) => X::X2(s),
  |               - data moved here
9 |         X::X2(s) => X::X1(s),
  |               - ...and here

Пожалуйста, не предлагайте такие вещи, как реализация enum X { X1, X2 } и использование struct S { variant: X, str: String } и т.д. Это упрощенный пример, представьте себе, что в вариантах есть много других полей и вы хотите переместить одно поле из одного варианта в другой.

Ответ 1

Это не работает, потому что мы не можем переместить s из &mut X

Тогда не делайте этого... возьмите структуру по значению и верните новую:

enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: X) -> X {
    match x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    }
}

В конечном счете, компилятор защищает вас, потому что если бы вы могли вывести строку из перечисления, то она была бы в некоем полусозданном состоянии. Кто будет отвечать за освобождение строки, если функция будет паниковать в этот момент? Должен ли он освободить строку в enum или строку в локальной переменной? Это не может быть и то и другое, поскольку двойное освобождение - это проблема безопасности памяти.

Если бы вам пришлось реализовать это с изменяемой ссылкой, вы могли бы временно сохранить фиктивное значение там:

use std::mem;

fn increment_x_inline(x: &mut X) {
    let old = mem::replace(x, X::X1(String::new()));
    *x = increment_x(old);
}

Создание пустой String не так уж плохо (это всего лишь несколько указателей, нет выделения кучи), но это не всегда возможно. В этом случае вы можете использовать Option:

fn increment_x_inline(x: &mut Option<X>) {
    let old = x.take();
    *x = old.map(increment_x);
}

Смотрите также:

Ответ 2

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

use std::mem;

#[derive(Debug)]
enum X {
    X1(String),
    X2(String),
}

fn increment_x(x: &mut X) {
    let interim = unsafe { mem::uninitialized() };
    let prev = mem::replace(x, interim);
    let next = match prev {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    };
    let interim = mem::replace(x, next);
    mem::forget(interim); // Important! interim was never initialized
}