Rust的移动和复制语义与C++非常不同。我将采用一种不同的方法来解释它们,而不是现有的答案。
在C++中,复制是一个可以任意复杂的操作,这是由于自定义的复制构造函数。Rust不希望简单赋值或参数传递具有自定义语义,因此采取了不同的方法。
首先,在Rust中,赋值或参数传递始终只是一个简单的内存复制。
let foo = bar
function(foo)
但是如果对象控制一些资源呢?假设我们正在处理一个简单的智能指针,
Box
。
let b1 = Box::new(42)
let b2 = b1
现在,如果只是简单地复制字节,那么析构函数(Rust中的drop
)不会被调用两次,从而导致未定义的行为吗?
答案是Rust默认情况下是移动操作。这意味着它将字节复制到新位置,旧对象就消失了。在上面的第二行之后访问b1
将导致编译错误。并且它的析构函数也不会被调用。该值已经移动到b2
,b1
实际上可能已经不存在了。
这就是Rust中移动语义的工作方式。字节被复制过去,旧对象就消失了。
在一些关于C++的移动语义的讨论中,Rust的方式被称为“破坏性移动”。有人提出了向C++添加“移动析构函数”或类似功能的建议,以使其具有相同的语义。但是C++中实现的移动语义并不是这样的。旧对象被留下,它的析构函数仍然会被调用。因此,您需要一个移动构造函数来处理移动操作所需的自定义逻辑。移动只是一种特殊的构造函数/赋值运算符,它被期望以特定的方式行为。
默认情况下,Rust的赋值操作会移动对象,使旧位置无效。但是许多类型(整数、浮点数、共享引用)具有复制字节的语义,这是一种创建真正副本的有效方式,不需要忽略旧对象。这些类型应该实现
Copy
特性,编译器可以自动派生它们。
#[derive(Clone, 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 { }
现在,这个有没有什么不好的地方呢?实际上,有一个相当大的缺点:因为将一个对象移动到另一个内存位置只是通过复制字节来完成,而没有自定义逻辑,所以一个类型
不能有对自身的引用。事实上,Rust的生命周期系统使得构造这样的类型变得不安全成为不可能。
但是在我看来,这种权衡是值得的。