是否可以使类型只能移动而不能复制?

时间:2014-06-16 22:47:05

标签: rust

  

编辑注释:在Rust 1.0之前询问了这个问题,并且问题中的一些断言在Rust 1.0中不一定正确。一些答案已经更新,以解决这两个版本。

我有这个结构

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}

如果我将它传递给函数,则会隐式复制它。现在,有时我读到某些值不可复制,因此必须移动。

是否可以使此结构Triplet不可复制?例如,是否可以实现一个特性,使Triplet不可复制,因此可以“移动”?

我在某处读过一个必须实现Clone特征来复制那些不可隐式复制的东西,但我从来没有读过另一种方式,那就是有一些可以隐式复制的东西并使它成为非可复制,以便它移动。

这甚至没有任何意义吗?

2 个答案:

答案 0 :(得分:162)

前言:这个答案是在opt-in built-in traits - 特别是the Copy aspects - 之前编写的。我使用了块引号来表示仅适用于旧方案的部分(在询问问题时应用的部分)。


  

:要回答基本问题,您可以添加一个存储NoCopy value的标记字段。 E.g。

     
struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}
     

您也可以通过使用析构函数(通过实现Drop trait)来实现,但如果析构函数不执行任何操作,则首选使用标记类型。

默认情况下,类型现在会移动,也就是说,当您定义新类型时,它不会实现Copy,除非您明确地为您的类型实现它:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move

只有新structenum中包含的每种类型都是Copy时,才能存在实施。如果没有,编译器将打印错误消息。只有当类型没有Drop实现时,它才会存在。


要回答你没有问过的问题......“移动和复制有什么用?”:

首先,我将定义两个不同的“副本”:

  • 一个字节拷贝,它只是逐字节地轻微复制一个对象,而不是跟随指针,例如如果你有(&usize, u64),它在64位计算机上是16个字节,浅拷贝将占用这16个字节并在其他一些16字节的内存块中复制它们的值,没有触摸usize另一端的&。也就是说,它相当于调用memcpy
  • 一个语义副本,复制一个值以创建一个新的(某种程度上)独立的实例,可以安全地单独使用到旧的实例。例如。 Rc<T>的语义副本仅涉及增加引用计数,Vec<T>的语义副本涉及创建新分配,然后将每个存储元素从旧文件复制到新文件。这些可以是深层副本(例如Vec<T>)或浅层(例如Rc<T>不接触存储的T),Clone被宽松地定义为所需的最小工作量从T内部将&T类型的值语义复制到T

Rust就像C,每个值的使用值都是一个字节副本:

let x: T = ...;
let y: T = x; // byte copy

fn foo(z: T) -> T {
    return z // byte copy
}

foo(y) // byte copy

无论T移动还是“隐式可复制”,它们都是字节副本。 (要清楚,它们在运行时不一定是字面上的逐字节副本:如果保留代码的行为,编译器可以自由地优化副本。)

但是,字节副本存在一个根本问题:你最终会在内存中出现重复值,如果它们有析构函数,那就非常糟糕了,例如

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here

如果w只是v的普通字节副本,则会有两个向量指向相同的分配,两个向量都会释放它们...导致a double free,是个问题。 NB。如果我们将v的语义副本放到w中,那么这将是完全没问题的,因为那时w将是它自己的独立Vec<u8>并且析构函数不会被践踏彼此。

这里有一些可能的修复:

  • 让程序员像C一样处理它(C中没有析构函数,所以它没那么糟糕......你只是留下了内存泄漏。:P)
  • 隐式执行语义复制,以便w有自己的分配,比如C ++及其副本构造函数。
  • 将按值使用视为所有权转移,以便v无法再使用,并且不会运行析构函数。

最后一个是Rust所做的: move 只是一个按值使用,其中源是静态无效的,因此编译器会阻止进一步使用现在无效的内存。

let v: Vec<u8> = vec![1, 2, 3];
let w: Vec<u8> = v;
println!("{}", v); // error: use of moved value

具有析构函数的类型必须在按值使用时(也就是字节复制时)移动,因为它们具有某些资源的管理/所有权(例如内存分配或文件句柄)及其字节副本不太可能正确复制此所有权。

“嗯......什么是隐含的副本?”

考虑像u8这样的原始类型:字节副本很简单,只需复制单个字节,语义副本就像复制单个字节一样简单。特别是,字节副本一个语义副本...... Rust甚至有一个built-in trait Copy来捕获哪些类型具有相同的语义和字节副本。

因此,对于这些Copy类型,按值使用也是自动语义副本,因此继续使用源代码是完全安全的。

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
  

NoCopy标记会覆盖编译器自动行为,即假设类型可以是Copy(即只包含基元和&的聚合)是Copy。但是,在opt-in built-in traits实施时,这将会发生变化。

如上所述,实现了opt-in内置特征,因此编译器不再具有自动行为。但是,过去用于自动行为的规则与检查实施Copy是否合法的规则相同。

答案 1 :(得分:6)

最简单的方法是在您的类型中嵌入不可复制的内容。

标准库为此用例提供了“标记类型”:NoCopy。例如:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}