在将字段移动到新变体时更改枚举变体

sin*_*nan 12 rust

我想在不使用任何克隆的情况下将旧变体的字段移动到新变体时更新枚举变体:

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),
    }
}
Run Code Online (Sandbox Code Playgroud)

这并不工作,因为我们不能移动s&mut X.

请不要建议实现enum X { X1, X2 }和使用struct S { variant: X, str: String }等等.这是一个简化的例子,想象在变体中有许多其他字段,并希望将一个字段从一个变量移动到另一个变体.

She*_*ter 12

这并不工作,因为我们不能移动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),
    }
}
Run Code Online (Sandbox Code Playgroud)

最终,编译器正在保护您,因为如果您可以将字符串移出枚举,那么它将处于某种半构造状态.如果功能在那个确切的时刻恐慌,谁将负责释放字符串?它应该释放枚举中的字符串还是局部变量中的字符串.它不能同时作为双重免费是一个内存安全问题.

如果你必须在可变引用上实现它,你可以暂时存储一个虚拟值:

use std::mem;

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

创建一个空String的并不是太糟糕(它只是几个指针,没有堆分配),但它并不总是可能的.在这种情况下,您可以使用Option:

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

  • 这个答案真的很全面!我会添加此链接:https://rust-unofficial.github.io/patterns/idioms/mem-replace.html。我认为这是一种常见的设计模式,类似于使用 Option::take 作为自定义枚举。 (2认同)

kra*_*lyk 6

如果你想在不以零成本的方式移出价值的情况下做到这一点,你必须求助于一些不安全的代码(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
}
Run Code Online (Sandbox Code Playgroud)

  • 虽然我完全同意人们应该始终非常小心不安全,但“未初始化”的这种特定用法会在发布模式下编译出来,并且“切换”操作会就地应用。然而,我希望看到这个意图的安全实现,这就是为什么我[提出 `std::mem::replace_with`](https://github.com/rust-lang/rfcs/pull/2490#issuecomment -459448752)。 (2认同)