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


96

编者注:在Rust 1.0之前曾问过这个问题,并且该问题中的某些断言在Rust 1.0中不一定是正确的。一些答案已经更新,可以解决两个版本。

我有这个结构

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

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

是否可以使该结构Triplet不可复制?例如,是否有可能实现一种特性,该特性将使Triplet不可复制并因此成为“可移动的”?

我在某处读到一个内容,即必须实现该Clone特性以复制不可隐式复制的内容,但我从未读过另一种方法,即具有可隐式复制的内容并使它不可复制,从而可以移动。

那有什么意义吗?


1
paulkoerbitz.de/posts/…。在此很好地解释了为什么要移动还是复制。
肖恩·佩里

Answers:


164

前言:这个答案写之前选择在内置特性 -特别Copy方面 -were实现。我使用块引号表示仅适用于旧方案的部分(在提出问题时适用的部分)。


:要回答基本问题,您可以添加存储NoCopy的标记字段。例如

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

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

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

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

仅当new struct或new中包含的每个类型enum都是其自身时,实现才可以存在Copy。如果不是,编译器将显示一条错误消息。它也可以只在类型存在具有Drop实现。


要回答您没有问的问题……“移动和复制有什么关系?”:

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

  • 一个字节的副本,它只是逐字节地浅层复制一个对象,而不跟随指针,例如,如果有(&usize, u64),则它在64位计算机上为16字节,而浅层副本将占用这16个字节并复制它们值在其他16字节的其他内存块中,而无需触摸usize的另一端&。也就是说,它等效于call memcpy
  • 一个语义复制,复制的值来创建一个新的(有点)独立,可以单独安全地用于旧的实例。例如,an的语义副本Rc<T>仅涉及增加引用计数,an 的语义副本Vec<T>涉及创建新分配,然后在语义上将每个存储的元素从旧复制到新。这些可以是深层副本(例如Vec<T>)或浅层副本(例如Rc<T>不接触存储的T),Clone松散地定义为T从a &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那么将有两个指向相同分配的向量,两个向量都带有释放它的析构函数...导致double free,这是一个问题。注意 这将是完全正常的,如果我们做的语义拷贝vw,从此w将自己的独立Vec<u8>和析构函数不会对互相践踏。

这里有一些可能的修复:

  • 让程序员像C一样处理它。(C中没有析构函数,所以也没有那么糟……您只剩下内存泄漏了。:P)
  • 隐式地执行语义复制,以便w具有自己的分配,例如C ++及其复制构造函数。
  • 将按值用途视为所有权转移,因此v不能再使用它,也不会运行其析构函数。

最后是Rust的工作:移动只是按值使用,其中源静态地无效,因此编译器禁止进一步使用现在无效的内存。

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

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

“那么……什么是隐式副本?”

考虑一下原始类型u8:字节复制很简单,只需复制一个字节,语义复制也很简单,只需复制一个字节。特别地,字节副本语义副本... Rust甚至具有内置的特征Copy,可以捕获哪些类型具有相同的语义和字节副本。

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

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

旧的:该NoCopy标记会覆盖编译器的自动行为,即假设可以的类型为Copy(即仅包含基元和的集合&Copy。但是,当实现了内置的内置特征时,情况将会改变。

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


@dbaupp:您是否会知道选择内置的特征在哪个版本的Rust中出现?我认为0.10。
Matthieu M.

@MatthieuM。它尚未实现,实际上最近对可选内置组件的设计提出了一些修订建议
休恩2014年

我认为旧的报价应该删除。
星际之门

1
#[derive(Copy,Clone)]应该在
Triplet

6

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

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

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

15
这对于Rust> = 1.0无效。
马尔巴博
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.