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

Chr*_*oph 93 rust

编者注:在Rust 1.0之前问过这个问题,问题中的一些断言在Rust 1.0中不一定正确.一些答案已更新,以解决这两个版本.

我有这个结构

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
}
Run Code Online (Sandbox Code Playgroud)

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

是否有可能使这个结构Triplet不可复制?例如,是否有可能实现一个Triplet不可复制的特性,因此可以"移动"?

我读到某个地方必须实现Clone特征来复制那些不能隐式复制的东西,但是我从来没有读过相反的东西,那就是有一些隐式可复制的东西,并使它不可复制,以便它移动.

这甚至有意义吗?

huo*_*uon 163

前言:这个答案是在选择加入内置特征之前编写的- 特别Copy方面 - 已经实现了.我已经使用块引号来指示仅应用于旧方案的部分(在询问问题时应用的部分).


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

struct Triplet {
    one: int,
    two: int,
    three: int,
    _marker: NoCopy
}
Run Code Online (Sandbox Code Playgroud)

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

类型现在默认移动,也就是说,当您定义新类型时,它不会实现,Copy除非您为类型显式实现它:

struct Triplet {
    one: i32,
    two: i32,
    three: i32
}
impl Copy for Triplet {} // add this for copy, leave it out for move
Run Code Online (Sandbox Code Playgroud)

只有当new struct或者包含的每个类型enum本身时,才能存在实现Copy.如果没有,编译器将打印错误消息.它也可以只在类型存在具有Drop实现.


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

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

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

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

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

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

foo(y) // byte copy
Run Code Online (Sandbox Code Playgroud)

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

然而,字节副本存在一个基本问题:你最终会在内存中出现重复值,如果它们有析构函数,那么这可能非常糟糕,例如:

{
    let v: Vec<u8> = vec![1, 2, 3];
    let w: Vec<u8> = v;
} // destructors run here
Run Code Online (Sandbox Code Playgroud)

如果w只是一个普通的字节副本,v则会有两个向量指向相同的分配,两个都有析构函数释放它...导致双重释放,这是一个问题.NB.如果我们做了vinto 的语义副本w,那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
Run Code Online (Sandbox Code Playgroud)

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

"嗯......什么是隐含的副本?"

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

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

let v: u8 = 1;
let w: u8 = v;
println!("{}", v); // perfectly fine
Run Code Online (Sandbox Code Playgroud)

:NoCopy标记覆盖编译器的自动行为,即假设可以是Copy(即仅包含基元的聚合和&)的类型Copy.但是,当实施选择加入的内置特征时,这将会改变.

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


Bur*_*hi5 6

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

标准库为此用例提供了"标记类型":NoCopy.例如:

struct Triplet {
    one: i32,
    two: i32,
    three: i32,
    nocopy: NoCopy,
}
Run Code Online (Sandbox Code Playgroud)

  • 这对Rust> = 1.0无效. (15认同)