如何强制移动实现了Copy trait的类型?

28

默认情况下,自定义类型通过默认赋值进行移动。通过实现Copy特性,我可以通过默认赋值获得"浅层复制语义"。通过实现Clone特性,我也可以获得"深层复制语义"。

是否有一种方法可以强制对Copy类型进行移动

我尝试使用move关键字和闭包(let new_id = move || id;),但是出现了错误消息。我还没有深入了解闭包,但是从这里和那里看到它们,我认为那应该行得通。


精确地将移动的变量标记为“未初始化”。也就是说,如果我强制从“复制”类型中移动,我会使源变量为空并且无法使用,其值由目标获取。希望我表达得正确^^' - Noein
1
除了要在程序中开放漏洞之外,将某些内容保持为未初始化状态并没有任何好处。我认为您需要告诉我们您想做这些事情的原因,因为这没有任何意义 :-) - Shepmaster
但在Rust中,未初始化的变量已经有了保护。 我只是想有时说“是的,这种类型是Copy,但我不再需要这个变量中的值了。 这个函数通过val参数传递,直接拿走它。” 然后,在调用函数或其他操作之后,尝试重复使用标识符而不给它另一个值将在编译时产生错误。就是这样。 - Noein
是的,我在这里有点赞同@Narfanar。如果没有实现复制/克隆,Rust 将默认移动。即使有复制/克隆的实现,也应该有可能强制使用确切的语义。不知道编译器查看一行代码时会执行什么似乎是非常不可见的,因为您必须神奇地知道复制是否被实现,以确定它是否将保留原始内容。 - stu
6个回答

51

我不太理解你的问题,但是你似乎很困惑。所以我会解决这个困惑的根源:

我认为我正确地理解了C++中的复制/移动概念,但是“所有东西都是一个memcpy”并不直观,每次读到时都无法理解。

在思考 Rust 的移动语义时,请忽略 C++。C++的故事比Rust更复杂,而Rust的故事则非常简单。但是,在C++的术语中解释Rust的语义会很混乱。

TL;DR:副本就是移动。移动就是副本。只有类型检查器知道区别。因此,当您想要为Copy类型“强制移动”时,您请求的是已经拥有的东西。

因此,我们有三种语义:

  • let a = b,其中b不是Copy
  • let a = b,其中bCopy
  • let a = b.clone(),其中bClone

注意:赋值和初始化之间没有实质性的区别(就像在C++中一样)- 赋值只是首先drop旧值。

注意:函数调用参数的工作方式与赋值相同。f(b)b分配给f的参数。


首要任务。

a = b始终执行memcpy

所有三种情况都是如此。

  • 当您执行let a = b时,b会被memcpya中。
  • 当您执行let a = b.clone()时,b.clone()的结果会被memcpya中。

移动

想象一下b是一个Vec。一个Vec看起来像这样:

{ &mut data, length, capacity }

当你写下let a = b时,你最终会得到:

b = { &mut data, length, capacity }
a = { &mut data, length, capacity }
这意味着ab都引用了&mut data,这意味着我们具有别名可变数据
类型系统不喜欢这样做,因此说我们不能再使用b。对b的任何访问都将在编译时失败。

注意:ab不必将堆数据命名为别名,以使同时使用两者成为不好的想法。例如,它们都可以是文件句柄,复制会导致文件被关闭两次。

注意:当涉及到析构函数时,移动确实具有额外的语义,但编译器不会让您在具有析构函数的类型上编写Copy


副本

想象一下,b是一个Option<i32>。一个Option<i32>看起来像这样:

{ is_valid, data }

当你写下 let a = b 时,你最终会得到:

b = { is_valid, data }
a = { is_valid, data }

它们可以同时使用。为了告诉类型系统这一点,需要将Option<i32>标记为Copy

注意:将某物标记为复制并不会改变代码的实际操作,它只允许更多的代码。如果您删除一个Copy实现,您的代码将出现错误或执行完全相同的操作。同样,将非Copy类型标记为Copy也不会改变任何编译后的代码。


克隆

假设要复制一个Vec。需要实现Clone,以生成一个新的Vec,然后执行:

let a = b.clone()

这个操作包含两个步骤。我们从以下开始:

b = { &mut data, length, capacity }

运行 b.clone() 会得到一个额外的右值临时对象

b = { &mut data, length, capacity }
    { &mut copy, length, capacity } // temporary

运行 let a = b.clone() 将此内容复制到 a 中:

b = { &mut data, length, capacity }
    { &mut copy, length, capacity } // temporary
a = { &mut copy, length, capacity }

由于 Vec 不是 Copy 类型,类型系统因此阻止了对临时变量的进一步访问。


但是效率呢?

到目前为止,我忽略了一件事,即移动和复制可以省略。Rust 保证某些平凡的移动和复制可以被省略。

由于编译器(在生命周期检查后)在两种情况下看到的结果相同,因此它们会以完全相同的方式被省略。


哦,好的回答!我现在明白了。虽然 clone 经过一番试验后,现在似乎是所有这些神奇的东西中最重要的部分。我尝试为本地类型实现它,就像 这里 中所实现的那样,但它会出错,因为我正在尝试通过值传递一个非复制类型,而我只是借用它。那么,对于任何复杂类型,克隆可能发生在哪里呢?!(注意:这不是我的设置中的错误;#[derive(...)] 可以正常工作。) - Noein
那个 Clone 实现只是重定向到 Copy。如果你的类型不可复制,你需要更聪明一些。例如,#[derive(clone)] 将会 Clone 每个结构体成员。 - Veedrac
但是...嗯...http://is.gd/r0OCcl ?! 如果我理解正确的话,看起来Copy依赖于Clone - Noein
1
@Noein Copy需要Clone,因为没有理由只实现Copy。如果有人想要接受T: Clone,那么如果他们不能接受Copy类型,那就太傻了。编译器错误只是为了防止人们忘记。 - Veedrac

12

将可复制类型包装在另一种不实现Copy的类型中。

struct Noncopyable<T>(T);

fn main() {
    let v0 = Noncopyable(1);
    let v1 = v0;
    println!("{}", v0.0); // error: use of moved value: `v0.0`
}

6

新回答

有时候我希望程序能够“大喊”:“在这里放入一个新的值!”

那么答案就是“不行”。当移动实现了Copy的类型时,源和目标始终都是有效的。当移动未实现Copy的类型时,源将永远无效,而目标始终有效。没有语法或特性可以表示“让我选择此实现了Copy的类型是否在此时充当Copy”。

原始回答

我只是想有时候说:“是的,这个类型是可复制的,但我真的不再需要这个变量中的值了。这个函数按值接受参数,就直接拿去吧。”

看起来你正在手动完成优化器的工作。不用担心,优化器会为你完成这项工作。这样做的好处是不需要担心它。


1
不,有时我只希望它对我大喊“在这里放一个值!”肯定不是在尝试优化;特别是考虑到我仍然不理解Copy!Copy底层的技术区别(即“所有东西都是memcpy;有时是浅复制,有时是深复制,但几乎没有直觉。”)。 - Noein
3
没错。即使回答不符合提问者的期望,那也是一个回答。:/ - Noein
1
@Noein 是的,但很有可能你提出这个问题是因为你有一个真正的问题。我想避免 XY 问题 - Shepmaster
1
也许最初的问题只是对“复制”似乎会很慢的反应。如果我复制一个文件,需要一段时间,但如果我移动一个文件,速度就很快。因此,试图防止复制可能是为了提高速度(尽管答案和评论表明没有速度提升)。 - jocull
也许你可以解释一下应该如何推断正在发生的事情?我是一个 Rust 新手,我认为重点是尽可能清晰和精确,以避免运行时出现问题。从你的话中可以看出,我可以看一行代码进行赋值,却不知道它是在复制并保留原始内容还是在移动并使原始内容无法使用。通过具有某些语法标记,作者将能够在代码中明确表示它是特别被移动的。 - stu
@stu Copy 特质存在的原因之一是对于某些类型,如普通整数或布尔值,留下不可用的东西是没有意义的。虽然你可能无法通过查看代码行来判断,但编译器会迅速提供错误提示,指出你正在尝试使用已移动的非 Copy 值,因此你不必自己思考。 - Shepmaster

6
移动和复制在底层基本上是相同的运行时操作。编译器会插入代码,从第一个变量地址开始进行按位复制到第二个变量的地址。在移动的情况下,编译器还会使第一个变量无效,这样如果以后再使用它就会出现编译错误。
即便如此,我认为如果Rust语言允许程序明确指定赋值是显式移动而不是复制仍然是有价值的。它可以通过防止对错误实例的无意引用来捕获bug。如果编译器知道您不需要两份副本并且可以调整绑定以避免按位复制,这也可能在某些情况下生成更有效的代码。
例如,如果您可以声明一个= move赋值或类似的内容。
let coord = (99.9, 73.45);
let mut coord2 = move coord;
coord2.0 += 100.0;
println!("coord2 = {:?}", coord2);
println!("coord = {:?}", coord); // Error

1
在 Rust 中,当您使用(或移动,用 Rust 的术语)一个值时,如果该值是 Copy,原始值仍然有效。如果您想模拟其他不可复制的值的情况,在特定使用后使其无效,可以执行以下操作:
let v = 42i32;
// ...
let m = v; 
// redefine v such that v is no longer a valid (initialized) variable afterwards
// Unfortunately you have to write a type here. () is the easiest,
// but can be used unintentionally.
let v: (); 
// If the ! type was stabilized, you can write
let v: !;
// otherwise, you can define your own:
enum NeverType {};
let v: NeverType;
// ...

如果您将 v 更改为不是 Copy 的内容,您无需更改上面的代码以避免使用移动后的值。

关于问题的一些误解更正

  • CloneCopy 的区别不在于 "浅拷贝" 和 "深拷贝" 语义。 Copy 是 "memcpy" 语义,而 Clone 则是实现者喜欢的任何语义,这是唯一的区别。尽管按定义需要 "深拷贝" 的事物无法实现 Copy

  • 当一个类型同时实现了 CopyClone,预期两者具有相同的语义,只是 Clone 可能会有副作用。对于实现了 Copy 的类型,它的 Clone 不应该具有 "深拷贝" 语义,克隆结果应该与复制结果相同。

  • 如果你想使用闭包来帮助,你可能想要 运行 闭包,例如 let new_id = (move || id)();。如果 id 是可复制的,那么移动后 id 仍然有效,所以这完全没有帮助。


“both have the same semantics except” — 我不同意:如果某个东西实现了 Copy,那么 Clone 应该始终委托给 Copy 实现。没有什么好的理由让 Clone 做与 Copy 不同的事情。 - Shepmaster
那么println!调用呢?这就是我所说的“副作用”。 - Earth Engine

1
运行时,在Rust中,复制移动有相同的效果。然而,在编译时,在移动的情况下,一个对象所在的变量被标记为不可用,但在复制的情况下不会。
当你使用Copy类型时,总是希望有值语义,而在不使用Copy类型时,则希望有对象语义
在Rust中,对象没有一致的地址:由于运行时行为的影响,地址经常在移动之间改变,即它们只属于一个绑定。这与其他语言非常不同!

2
对于像我这样的 Rust 初学者来说,当你说“由于运行时,对象没有一致的地址”时,这似乎会导致额外的分配和复制,以确保只有一个绑定拥有内存,因此效率会大大降低。 - johnbakers

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