Rust 的移动和复制语义与 C++ 非常不同。我将采用与现有答案不同的方法来解释它们。
在 C++ 中,由于自定义的复制构造函数,复制是一种可以任意复杂的操作。 Rust 不想要简单赋值或参数传递的自定义语义,因此采用了不同的方法。
首先,在 Rust 中传递的赋值或参数始终只是一个简单的内存副本。
let foo = bar; // copies the bytes of bar to the location of foo (might be elided)
function(foo); // copies the bytes of foo to the parameter location (might be elided)
但是如果对象控制了一些资源呢?假设我们正在处理一个简单的智能指针Box。
let b1 = Box::new(42);
let b2 = b1;
此时,如果只复制字节,是否会为每个对象调用析构函数(Rust 中的drop),从而释放相同的指针两次并导致未定义的行为?
答案是 Rust 默认会移动。这意味着它将字节复制到新位置,然后旧对象就消失了。在上面第二行之后访问b1 是编译错误。并且不需要析构函数。该值已移至b2,而b1 可能不再存在。
这就是 Rust 中移动语义的工作方式。字节被复制过来,旧对象消失了。
在一些关于 C++ 移动语义的讨论中,Rust 的方式被称为“破坏性移动”。已经有人提议添加“移动析构函数”或类似于 C++ 的东西,以便它可以具有相同的语义。但是移动语义在 C++ 中实现时不会这样做。旧的对象被留下了,它的析构函数仍然被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一个专门的构造函数/赋值运算符,预期会以某种方式运行。
所以默认情况下,Rust 的赋值会移动对象,使旧位置无效。但是许多类型(整数、浮点、共享引用)具有复制字节的语义是创建真实副本的一种完全有效的方式,无需忽略旧对象。这样的类型应该实现Copy trait,它可以由编译器自动派生。
#[derive(Copy)]
struct JustTwoInts {
one: i32,
two: i32,
}
这向编译器表明赋值和参数传递不会使旧对象无效:
let j1 = JustTwoInts { one: 1, two: 2 };
let j2 = j1;
println!("Still allowed: {}", j1.one);
请注意,琐碎的复制和销毁的需要是相互排斥的; Copy 的类型 不能 也可以是 Drop。
现在,当您想要复制仅复制字节还不够的内容时,例如一个向量?这没有语言功能;从技术上讲,该类型只需要一个返回以正确方式创建的新对象的函数。但按照惯例,这是通过实现 Clone 特征及其 clone 函数来实现的。事实上,编译器也支持Clone 的自动派生,它只是克隆每个字段。
#[Derive(Clone)]
struct JustTwoVecs {
one: Vec<i32>,
two: Vec<i32>,
}
let j1 = JustTwoVecs { one: vec![1], two: vec![2, 2] };
let j2 = j1.clone();
无论何时你派生Copy,你也应该派生Clone,因为像Vec这样的容器在克隆它们自己时会在内部使用它。
#[derive(Copy, Clone)]
struct JustTwoInts { /* as before */ }
现在,这有什么缺点吗?是的,实际上有一个相当大的缺点:因为将对象移动到另一个内存位置只是通过复制字节来完成,没有自定义逻辑,cannot have references into itself 类型。事实上,Rust 的生命周期系统使得安全地构造此类类型变得不可能。
但在我看来,这种权衡是值得的。