能否将类型仅限制为可移动而非可复制?

98

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

我有这个结构体

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

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

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

我曾经在某个地方读到,必须实现Clone trait才能复制那些不可隐式复制的东西,但我从未读到过相反的情况,即将可隐式复制的内容变为不可复制,以便它被移动。

这有任何意义吗?


1
这里有一篇关于 Rust 中指针、所有权和生命周期的理解的好文章,很好地解释了为什么要移动而不是复制。http://paulkoerbitz.de/posts/Understanding-Pointers-Ownership-and-Lifetimes-in-Rust.html - Sean Perry
2个回答

177
前言:本答案是在 内置特质中的 opt-in,尤其是 Copy 特质 实现之前编写的。我使用块引用来指示仅适用于旧方案的部分(即在提问时适用的方案)。

Old: To answer the basic question, you can add a marker field storing a NoCopy value. E.g.

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

You can also do it by having a destructor (via implementing the Drop trait), but using the marker types is preferred if the destructor is doing nothing.

现在类型默认移动,也就是说,当你定义一个新类型时,它不会实现 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的一个普通字节复制,那么将会有两个指向同一分配的向量,它们都有析构函数释放它...导致双重释放,这是一个问题。请注意,如果我们对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甚至有一个内置特质Copy,用于标识哪些类型具有相同的语义和字节复制。

因此,对于这些Copy类型的按值使用也自动成为语义复制,因此继续使用源是完全安全的。

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

旧文:`NoCopy` 标记覆盖了编译器的自动行为,即假定只包含原始类型和 `&` 的聚合体类型是可复制的(即 `Copy`)。然而,当实现 选择内置特性 时,这将发生变化。
如上所述,已经实现了选择内置特性,因此编译器不再具有自动行为。但是,过去用于自动行为的规则与检查是否可以实现 `Copy` 的规则相同。

6

最简单的方法是在您的类型中嵌入一个不可复制的东西。

标准库提供了一个“标记类型”来满足这个用例:NoCopy。例如:

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

18
对于 Rust >= 1.0,此内容不再有效。 - malbarbo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接