Rust language website声称move语义是该语言的特性之一,但我看不出move语义是如何在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根本没有构造函数,更不用说移动构造函数了。
- 不支持右值引用。
- 无法使用右值参数创建函数重载。
Rust如何提供移动语义?
6条答案
按热度按时间cs7cruho1#
我认为这是C的一个常见问题。在C中,当涉及到复制和移动时,你会显式地做所有事情。这门语言是围绕复制和引用而设计的。在C++11中,“移动”东西的能力被粘在了那个系统上。另一方面,Rust有了一个新的开始。
Rust根本没有构造函数,更不用说移动构造函数了。
你不需要移动构造函数。Rust移动所有“没有复制构造函数”的东西,也就是“没有实现
Copy
特征”。Rust的默认构造函数(按照惯例)只是一个名为
new
的关联函数:更复杂的构造函数应该有更有表现力的名字。这是C++中的命名构造函数习惯用法
不支持右值引用。
这一直是一个要求的功能,请参阅RFC issue 998,但很可能您要求的是一个不同的功能:将内容移至函数:
无法使用右值参数创建函数重载。
你可以用特质来做。
qcbq4gxm2#
Rust的移动和复制语义与C有很大的不同,我将采用一种不同于现有答案的方法来解释它们。
在C中,复制是一个可以任意复杂的操作,这是由于自定义的复制构造函数。Rust不需要简单赋值或参数传递的自定义语义,因此采用了不同的方法。
首先,Rust中传递的赋值或参数总是简单的内存复制。
但是如果对象控制了一些资源呢?假设我们正在处理一个简单的智能指针
Box
。此时,如果只复制字节,是否会为每个对象调用析构函数(Rust中的
drop
),从而释放同一个指针两次并导致未定义的行为?答案是Rust * 默认移动 *。这意味着它将字节复制到新的位置,旧的对象随之消失。在上面第二行之后访问
b1
是一个编译错误。并且没有为它调用析构函数。值被移动到b2
,b1
可能不再存在。这就是移动语义在Rust中的工作方式。字节被复制,旧的对象消失。
在一些关于C的移动语义的讨论中,Rust的方法被称为“破坏性移动”。有人建议在C中添加“移动析构函数”或类似的东西,这样它就可以具有相同的语义。但是在C++中实现的移动语义并不这样做。旧的对象被留下,它的析构函数仍然被调用。因此,你需要一个移动构造函数来处理移动操作所需的自定义逻辑。2移动只是一个特殊的构造函数/赋值运算符,它应该以某种方式来表现。
因此默认情况下,Rust的赋值会移动对象,使旧的位置无效。但许多类型(整数、浮点、共享引用)都有这样的语义,即复制字节是创建真实的副本的一种完全有效的方式,而无需忽略旧的对象。此类类型应该实现
Copy
特征,该特征可以由编译器自动派生。这会向编译器发出信号,告知赋值和参数传递不会使旧对象失效:
注意,琐碎的复制和销毁的需要是相互排斥的;
Copy
* 类型不能 * 也是Drop
。现在,当你想要复制一个仅仅复制字节是不够的东西,比如一个向量,怎么办呢?从技术上讲,该类型只需要一个函数,返回一个以正确方式创建的新对象。但按照惯例,这是通过实现
Clone
特征及其clone
函数来实现的。实际上,编译器也支持Clone
的自动派生,它只是简单地克隆每个字段。无论何时派生
Copy
,都应该同时派生Clone
,因为像Vec
这样的容器在克隆自身时会在内部使用它。现在,这有什么缺点吗?是的,事实上有一个相当大的缺点:因为将一个对象移动到另一个内存位置只是通过复制字节来完成的,而没有自定义逻辑,类型cannot have references into itself。
但在我看来,这种取舍是值得的。
ycl3bljg3#
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++中相同的性能优势,以及一些其他优势。
ezykj2lf4#
这就是它在内存中的表示方式
然后让我们将s赋值给t
这就是发生的事情:
let t = s
将向量的三个报头字段从s移动到t;现在,t是向量的所有者,向量的元素保持在原来的位置,字符串也没有任何变化,每个值仍然有一个所有者。现在s被释放了,如果我写这个
我收到错误:移动值使用:
s
英寸Rust将move语义应用到几乎任何值的使用上(除了Copy类型)。从一个函数返回一个值会把所有权转移到调用者。2构建一个元组会把值转移到元组中。3等等。
Reference:Programming Rust by Jim Blandy, Jason Orendorff, Leonora F. S. Tindall
ovfsdjhp5#
我想补充的是,移动到
memcpy
并不是必须的。如果堆栈上的对象足够大,Rust的编译器可能会选择传递对象的指针。3xiyfsfu6#
在C中,类和结构体的默认赋值是浅层复制。复制的是值,而不是指针引用的数据。因此,修改一个示例会更改所有副本的引用数据。值(例如用于给药)在另一种情况下保持不变,可能呈现不一致的状态。移动语义可以避免这种情况。具有移动语义的内存托管容器的C实现示例:
这样的对象会自动被垃圾回收,并可以从函数返回到调用程序。它非常高效,并且和Rust做的一样: