【问题标题】:Change enum variant while moving the field to the new variant在将字段移动到新变体时更改枚举变体
【发布时间】:2021-12-31 13:50:52
【问题描述】:

我想更新枚举变体,同时将旧变体的字段移动到新变体而不进行任何克隆:

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 } 等的建议。这是一个简化的示例,想象一下变体中有很多其他字段,并且想要将一个字段从一个变体移动到另一个。

【问题讨论】:

  • String的情况下,您可以在mem::replace字段中输入一个空字符串,并使用结果形成新的变体。只是几个动作。但这仅适用于具有廉价形式(如空字符串)的类型。
  • 仅供参考,我有proposed std::mem::replace_with,在这种情况下应该会有所帮助。

标签: rust


【解决方案1】:

在某些特定情况下,你想要的其实是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())},
    }
}

【讨论】:

    【解决方案2】:

    这不起作用,因为我们无法将 s&amp;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),
        }
    }
    

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

    如果您不得不在可变引用上实现它,您可以在其中临时存储一个虚拟值:

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

    另见:

    【讨论】:

    【解决方案3】:

    如果您想在不以零成本方式摆脱价值的情况下做到这一点,则必须求助于一些不安全的代码 (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
    }
    

    【讨论】:

    • 这里你必须非常小心。在mem::uninitialized()mem::forget 之间发生的任何恐慌都将允许未初始化的值泄漏到程序的其余部分。在这个例子中,没有看到这种可能性。但是,很可能有人修改了示例以便能够调用可能会出现恐慌的代码。 FWIW,我有extracted the second mem::replace
    • 显然,无论如何你都需要非常小心unsafe 代码。我需要在性能关键路径中执行此操作,因此 unsafe 是要走的路。如果有人想在其中插入一些重要/可能令人恐慌的代码,他们应该制作一个“忘记对象”,即。一个存储临时对象并在drop() 上忘记它的对象。
    • 虽然我完全同意人们应该始终非常小心 unsafe,但 uninitialized 的这种特定用法会在发布模式下编译出来,并且“切换”操作会就地应用。然而,我希望看到这个意图的安全实施,这就是我proposed std::mem::replace_with的原因。
    猜你喜欢
    • 2016-01-07
    • 1970-01-01
    • 2021-07-04
    • 2021-01-03
    • 1970-01-01
    • 2019-08-10
    • 2013-01-04
    相关资源
    最近更新 更多