在发生改变的枚举中使用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
来替换它的值,Option
的take()
方法提供了一个更短和更习惯的替代方法。
优势
没有内存分配。
劣势
表达比较啰嗦,经常搞错会让你讨厌借用检查器。 编译器可能无法优化掉双重存储,从而导致性能下降,这与你在不安全语言中的做法是不同的。
此外,你拿走的类型需要实现Default
trait。
如果你正在使用的类型没有实现,你可以使用mem::replace
代替。
讨论
这种模式只在Rust中才有意义。 在有垃圾回收的语言中,默认取值的引用(GC会跟踪引用),而在其他低级语言如C语言中,可以简单地别名指针,并在以后修复。
然而,在Rust中,我们必须多做一点工作才能做到这一点。一个所有值可能只有一个所有者,所以要把它取出来,我们需要把一些东西放回去。
参见
在特定情况下,可以去除通过Clone来满足借用检查器的反面模式。
Latest commit 9834f57 on 25 Aug 2021