[英]How does Rust provide move semantics?
Rust 语言网站声称移动语义是该语言的特征之一。 但是我看不到移动语义在 Rust 中是如何实现的。
Rust 框是唯一使用移动语义的地方。
let x = Box::new(5);
let y: Box<i32> = x; // x is 'moved'
上面Rust的代码可以写成C++为
auto x = std::make_unique<int>(5);
auto y = std::move(x); // Note the explicit move
据我所知(如果我错了请纠正我),
Rust 如何提供移动语义?
我认为这是来自 C++ 的一个非常普遍的问题。 在 C++ 中,当涉及到复制和移动时,您正在明确地做所有事情。 该语言是围绕复制和引用而设计的。 在 C++11 中,“移动”东西的能力被粘在了那个系统上。 另一方面,Rust 重新开始。
Rust 根本没有构造函数,更不用说移动构造函数了。
您不需要移动构造函数。 Rust 移动所有“没有复制构造函数”的东西,也就是“没有实现Copy
trait”。
struct A;
fn test() {
let a = A;
let b = a;
let c = a; // error, a is moved
}
Rust 的默认构造函数(按照惯例)只是一个名为new
的关联函数:
struct A(i32);
impl A {
fn new() -> A {
A(5)
}
}
更复杂的构造函数应该有更具表现力的名称。 这是 C++ 中的命名构造函数习语
不支持右值引用。
它一直是一个请求的功能,请参阅RFC 问题 998 ,但很可能您要求的是不同的功能:将内容移动到函数:
struct A;
fn move_to(a: A) {
// a is moved into here, you own it now.
}
fn test() {
let a = A;
move_to(a);
let c = a; // error, a is moved
}
无法使用右值参数创建函数重载。
你可以用特质来做到这一点。
trait Ref {
fn test(&self);
}
trait Move {
fn test(self);
}
struct A;
impl Ref for A {
fn test(&self) {
println!("by ref");
}
}
impl Move for A {
fn test(self) {
println!("by value");
}
}
fn main() {
let a = A;
(&a).test(); // prints "by ref"
a.test(); // prints "by value"
}
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
trait 及其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 */ }
现在,这有什么缺点吗? 是的,实际上有一个相当大的缺点:因为将一个对象移动到另一个内存位置只是通过复制字节来完成的,没有自定义逻辑,一个类型不能引用到它自己。 事实上,Rust 的生命周期系统使得安全地构造这样的类型是不可能的。
但在我看来,这种权衡是值得的。
Rust 支持具有以下功能的移动语义:
所有类型都是可移动的。
默认情况下,在整个语言中向某处发送一个值是一种移动。 对于非Copy
类型,如Vec
,以下都是 Rust 中的动作:按值传递参数、返回值、赋值、按值模式匹配。
您在 Rust 中没有std::move
,因为它是默认设置。 你真的一直在使用动作。
Rust 知道不能使用移动的值。 如果您有一个值x: String
并执行channel.send(x)
,将值发送到另一个线程,编译器就会知道x
已被移动。 在移动后尝试使用它是一个编译时错误,“使用移动的值”。 如果有人引用某个值(悬空指针),则您无法移动该值。
Rust 知道不要在移动的值上调用析构函数。 移动值会转移所有权,包括清理责任。 类型不必能够表示特殊的“值已移动”状态。
移动成本低,性能可预测。 它基本上是 memcpy。 返回一个巨大的Vec
总是很快——你只是复制三个词。
Rust 标准库在任何地方都使用并支持移动。 我已经提到了通道,它使用移动语义来安全地跨线程传输值的所有权。 其他优点:所有类型都支持 Rust 中的无副本std::mem::swap()
; Into
和From
标准转换特性是按值的; Vec
和其他集合具有.drain()
和.into_iter()
方法,因此您可以粉碎一个数据结构,将所有值移出其中,并使用这些值构建一个新的数据结构。
Rust 没有移动引用,但移动是 Rust 中一个强大且核心的概念,它提供了许多与 C++ 中相同的性能优势,以及其他一些优势。
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
这就是它在 memory 中的表示方式
然后让我们将 s 分配给 t
let t = s;
这是发生了什么:
let t = s
将向量的三个 header 字段从 s 移动到 t; 现在 t 是向量的所有者。 矢量的元素保持原样,字符串也没有任何变化。 每个值仍然只有一个所有者。
现在 s 被释放了,如果我写这个
let u = s
我收到错误:“使用移动值: s
”
Rust 将移动语义应用于几乎所有值的使用(复制类型除外)。 将 arguments 传递给函数会将所有权转移到函数的参数; 从 function 返回一个值将所有权转移给调用者。 构建元组将值移动到元组中。 等等。
参考例如:Jim Blandy、Jason Orendorff、Leonora FS Tindall 的编程 Rust
原始类型不能为空并且大小固定,而非原始类型可以增长并且可以为空。 由于基本类型不能为空且大小固定,因此分配 memory 来存储它们并处理它们相对容易。 然而,非基元的处理涉及计算随着它们的增长将占用多少 memory 以及其他昂贵的操作。Wwith primitives rust 将制作副本,非原始 rust 执行移动
fn main(){
// this variable is stored in stack. primitive types are fixed size, we can store them on stack
let x:i32=10;
// s1 is stored in heap. os will assign memory for this. pointer of this memory will be stored inside stack.
// s1 is the owner of memory space in heap which stores "my name"
// if we dont clear this memory, os will have no access to this memory. rust uses ownership to free the memory
let s1=String::from("my name");
// s1 will be cleared from the stack, s2 will be added to the stack poniting the same heap memory location
// making new copy of this string will create extra overhead, so we MOVED the ownership of s1 into s2
let s2=s1;
// s3 is the pointer to s2 which points to heap memory. we Borrowed the ownership
// Borrowing is similar borrowing in real life, you borrow a car from your friend, but its ownership does not change
let s3=&s2;
// this is creating new "my name" in heap and s4 stored as the pointer of this memory location on the heap
let s4=s2.clone()
}
当我们将原始或非原始类型 arguments 传递给 function 时,同样的原则适用:
fn main(){
// since this is primitive stack_function will make copy of it so this will remain unchanged
let stack_num=50;
let mut heap_vec=vec![2,3,4];
// when we pass a stack variable to a function, function will make a copy of that and will use the copy. "move" does not occur here
stack_var_fn(stack_num);
println!("The stack_num inside the main fn did not change:{}",stack_num);
// the owner of heap_vec moved here and when function gets executed, it goes out of scope so the variable will be dropped
// we can pass a reference to reach the value in heap. so we use the pointer of heap_vec
// we use "&"" operator to indicate that we are passing a reference
heap_var_fn(&heap_vec);
println!("the heap_vec inside main is:{:?}",heap_vec);
}
// this fn that we pass an argument stored in stack
fn stack_var_fn(mut var:i32){
// we are changing the arguments value
var=56;
println!("Var inside stack_var_fn is :{}",var);
}
// this fn that we pass an arg that stored in heap
fn heap_var_fn(var:&Vec<i32>){
println!("Var:{:?}",var);
}
我想补充一点,没有必要转移到memcpy
。 如果堆栈上的对象足够大,Rust 的编译器可能会选择传递对象的指针。
在 C++ 中,类和结构的默认分配是浅拷贝。 值被复制,但不是指针引用的数据。 所以修改一个实例会改变所有副本的引用数据。 值(fe 用于管理)在另一个实例中保持不变,可能导致不一致的状态。 移动语义避免了这种情况。 具有移动语义的内存管理容器的 C++ 实现示例:
template <typename T>
class object
{
T *p;
public:
object()
{
p=new T;
}
~object()
{
if (p != (T *)0) delete p;
}
template <typename V> //type V is used to allow for conversions between reference and value
object(object<V> &v) //copy constructor with move semantic
{
p = v.p; //move ownership
v.p = (T *)0; //make sure it does not get deleted
}
object &operator=(object<T> &v) //move assignment
{
delete p;
p = v.p;
v.p = (T *)0;
return *this;
}
T &operator*() { return *p; } //reference to object *d
T *operator->() { return p; } //pointer to object data d->
};
这样的对象会被自动垃圾回收,并且可以从函数返回给调用程序。 它非常高效,并且与 Rust 的功能相同:
object<somestruct> somefn() //function returning an object
{
object<somestruct> a;
auto b=a; //move semantic; b becomes invalid
return b; //this moves the object to the caller
}
auto c=somefn();
//now c owns the data; memory is freed after leaving the scope
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.