如何在将字段移动到新变量时更改枚举变量?

28

我想在不进行任何克隆的情况下,将旧变量的字段移动到新变量中并更新枚举变量:

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),
    }
}

这不起作用是因为我们无法从&mut X移动s

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 } 等方式。这只是一个简单的例子,想象一下在各种变量中有很多其他字段,并且想将一个字段从一个变量移到另一个变量。


3
对于 String 类型,你可以使用 mem::replace 将空字符串替换到字段中,然后使用结果形成新的变量。只需要几个步骤。但是这仅适用于像空字符串这样具有廉价形式的类型。 - Sebastian Redl
1
我已经提出了std::mem::replace_with,这应该有助于处理这种情况。 - Vlad Frolov
4个回答

28

这样做行不通,因为我们不能从&mut X中移动s

那就别这么做了...按值获取结构体并返回一个新的:

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),
    }
}

最终,编译器保护了你。因为如果你可以将字符串移出枚举,那么它可能处于某种半构造状态。如果函数在正好那个时刻发生恐慌,谁会负责释放那个字符串呢?它应该释放枚举中的字符串还是局部变量中的字符串?双重释放会导致内存安全问题,所以两者都不行。

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

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
这个答案真的很全面!不过我想补充这个链接:https://rust-unofficial.github.io/patterns/idioms/mem-replace.html。我认为这是一种常见的设计模式,类似于使用Option :: take来处理自定义枚举类型。 - rambi

5
如果您想以零成本的方式完成此操作,而又不想移动值,则需要使用一些不安全的代码(据我所知):
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
}

1
在这里你必须非常小心。在mem::uninitialized()mem::forget之间发生的任何恐慌都会导致未初始化的值泄漏到程序的其余部分。在这个例子中,没有看到这样的可能性。然而,很可能有人修改这个例子以便能够调用可能会引起恐慌的代码。FWIW,我会提取第二个mem::replace - Shepmaster
显然,无论如何,您都需要非常小心处理unsafe代码。我需要在性能关键路径中执行此操作,因此unsafe是正确的选择。如果有人想在其中插入一些非平凡/潜在恐慌的代码,他们应该制作一个“遗忘对象”,即存储临时对象并在drop()上将其遗忘。 - kralyk
2
虽然我完全同意应该始终非常小心地使用不安全的代码,但是在发布模式下,这个特定的uninitialized用法会被编译器优化掉,并且“toggle”操作会就地应用。然而,我很想看到一个安全的实现方式,这就是为什么我提出了std::mem::replace_with的原因。 - Vlad Frolov
我会非常小心处理这个。请查看有关mem::uninitialized警告的内容,了解为什么在几乎所有情况下都会导致立即未定义行为。未来的LLVM优化可能会破坏您的程序。 - WorldSEnder
1
使用 MaybeUninit 的更新版本。请注意,此版本通过了 miri 测试,而答案中的版本未能通过。 - WorldSEnder
一个更好的方法,不涉及使用未初始化的值,是对字段使用 mem::ManuallyDrop<String>,然后在重新分配之前使用 unsafe { ManuallyDrop::take(field) },只要您不在重新分配之前使用该引用即可。 - Amin Sameti

2
在某些特定情况下,您实际需要使用的是 std::rc
enum X {
    X1(Rc<String>),
    X2(Rc<String>),
}

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

0

正如@VladFrolov所评论的那样,曾经有一个RFC提议,旨在向标准库中添加一种方法std::mem::replace_with,它允许您暂时拥有可变引用背后的值的所有权。然而,它并未被接受。

有第三方crate提供类似的功能:take-mutreplace-with是我知道的著名的crate。通过阅读文档,您可能会看到为什么这不被接受到标准库中。如果给定所有权的函数发生panic,中止程序可能会导致潜在的严重后果,因为在继续展开之前需要将某些值放回可变引用中。除了panic之外,还有其他机制可用于“放回”值。

这里是使用replace-with的示例:

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

fn increment_x(x: &mut X) {
    replace_with::replace_with_or_abort(x, |x| match x {
        X::X1(s) => X::X2(s),
        X::X2(s) => X::X1(s),
    });
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接