在发生改变的枚举中使用mem::{take(_), replace(_)}来保留所有值

描述

假定我们有一个&mut MyEnum,它有(至少)两个变体, A { name: String, x: u8 }B { name: String }。 现在我们想如果x为零,把MyEnum::A改成B,同时保持MyEnum::B不变。

我们可以在不克隆name的情况下做到这一点。

例子


#![allow(unused)]
fn main() {
use std::mem;

enum MyEnum {
    A { name: String, x: u8 },
    B { name: String }
}

fn a_to_b(e: &mut MyEnum) {
    if let MyEnum::A { name, x: 0 } = e {
        // this takes out our `name` and put in an empty String instead
        // (note that empty strings don't allocate).
        // Then, construct the new enum variant (which will
        // be assigned to `*e`).
        *e = MyEnum::B { name: mem::take(name) }
    }
}
}

这也适用于更多的变体:


#![allow(unused)]
fn main() {
use std::mem;

enum MultiVariateEnum {
    A { name: String },
    B { name: String },
    C,
    D
}

fn swizzle(e: &mut MultiVariateEnum) {
    use MultiVariateEnum::*;
    *e = match e {
        // Ownership rules do not allow taking `name` by value, but we cannot
        // take the value out of a mutable reference, unless we replace it:
        A { name } => B { name: mem::take(name) },
        B { name } => A { name: mem::take(name) },
        C => D,
        D => C
    }
}
}

动机

在处理枚举时,我们可能想在原地改变一个枚举值,也许是改变成另一个变体。 为了通过借用检查器,这通常分两个阶段进行。 在第一阶段,我们观察现有值,看看它的各个部分,以决定下一步该做什么 在第二阶段,我们可以有条件地改变该值(如上面的例子)。

借用检查器不允许我们取走枚举类型的name(因为something必须存在。) 尽管我们可以.clone()``name然后将克隆值放入MyEnum::B中,但这就是反面模式通过Clone来满足借用检查器 的一个例子了。 无论如何,我们可以通过只用一个可变借用来改变e,进而避免额外的内存分配。

mem::take可以换掉这个值,用它的默认值代替,并返回之前的值。 对于String,默认值是一个空的String,不需要分配内存。 最终,我们得到了原来的name作为一个所有值。然后我们可以把它包在另一个枚举中。

注意: mem::replace非常相似,但允许我们指定用什么来替换值。 mem::take等价于mem::replace(name, String::new()).

但是请注意,如果我们使用一个Option,并想用一个None来替换它的值,Optiontake()方法提供了一个更短和更习惯的替代方法。

优势

没有内存分配。

劣势

表达比较啰嗦,经常搞错会让你讨厌借用检查器。 编译器可能无法优化掉双重存储,从而导致性能下降,这与你在不安全语言中的做法是不同的。

此外,你拿走的类型需要实现Defaulttrait。 如果你正在使用的类型没有实现,你可以使用mem::replace代替。

讨论

这种模式只在Rust中才有意义。 在有垃圾回收的语言中,默认取值的引用(GC会跟踪引用),而在其他低级语言如C语言中,可以简单地别名指针,并在以后修复。

然而,在Rust中,我们必须多做一点工作才能做到这一点。一个所有值可能只有一个所有者,所以要把它取出来,我们需要把一些东西放回去。

参见

在特定情况下,可以去除通过Clone来满足借用检查器的反面模式。

Latest commit 9834f57 on 25 Aug 2021