< 返回版块

lllhuaer 发表于 2021-12-28 14:23

move, copy, clone

原文:https://hashrust.com/blog/moves-copies-and-clones-in-rust/

译者:韩玄亮(一个热爱开源,喜欢 rust 的 go 开发者)

本文对 move, copy, clone 不做中文翻译,保持在 Rust 中的味道,翻译了就没哪味。

介绍

移动和复制是 Rust 中的基本概念。对于来自 Ruby、Python 或 C# 等具有垃圾回收功能语言的开发者来说,这些概念可能是完全陌生的。虽然这些术语在 C++ 中是存在的,但它们在 Rust 中的含义略有不同。在这篇文章中,我将解释在 Rust 中 move、copy 和 clone 的含义。就让我们一探究竟吧。

Move

正如「Rust 中的内存安全 - 2」所说的,将一个变量赋值给另一个变量会将所有权转移。

let v: Vec = Vec::new(); let v1 = v; // v1 is the new owner

在上述例子中,v 被移到了 v1 上。但移动 v 是什么意思?为了理解这一点,我们需要看看 Vec 在内存中的布局结构:

Vec 内部维护一个动态增长或收缩的缓冲区。这个缓冲区是在堆上分配的,包含 Vec 的实际元素。此外,Vec 在栈上还有一个小对象。这个对象包含一些管理信息:一个指向堆上缓冲区的指针,缓冲区的容量和长度(即当前有多少部分已经被填满)。

当变量 v 被移动到 v1 时,栈上的对象按位复制 (stack copy):

📒 : 在上面的例子中,实际上发生的是一个浅拷贝(也就是按位复制)。这与 C++ 截然然不同,C++ 在执行一个 vector 赋值给另一个变量时会进行深拷贝。堆上的缓冲区保持不变。但这里发生了一次移动:现在是 v1 负责释放堆上缓冲区,而不是 v:

let v: Vec = Vec::new(); let v1 = v; println!("v's length is {}", v.len()); // error: borrow of moved value: v

这种所有权的改变是有益的,因为如果同时允许通过 v 和 v1 访问缓冲区数据,那么你将会得到两个栈对象指向同一个堆缓冲区:

在这种情况下,哪个对象有权释放缓冲区?这点并不清楚,而 Rust 从根本就防止了这种情况的出现。(即:赋值 → 栈对象拷贝,同时转移所有权,保证一个对象在同一时间只能有一个所有者)

当然,赋值并不是唯一涉及移动的操作。值在作为参数传递或从函数返回时也会被移动:

let v: Vec = Vec::new(); // v is first moved into print_len's v1 // and then moved into v2 when print_len returns it let v2 = print_len(v); fn print_len(v1:Vec) ->Vec { println!("v1's length is {}", v1.len()); v1 // v1 is moved out of the function }

或是赋给结构体或 enum 的成员:

struct Numbers { nums:Vec } let v: Vec = Vec::new(); // v moved into nums field of the Numbers struct let n = Numbers { nums: v };

enum NothingOrString { Nothing, Str(String) } let s: String = "I am moving soon".to_string(); // s moved into the enum let nos = NothingOrString::Str(s);

这都是关于 move 的内容。接下来让我们看看 copy。

Copy

还记得上面的例子吗?

let v: Vec = Vec::new(); let v1 = v; println!("v's length is {}", v.len()); //error: borrow of moved value: v

如果我们把变量 v 和 v1 的类型从 Vec 改为 i32,会发生什么呢?

let v: i32 = 42; let v1 = v; println!("v is {}", v); // compiles fine, no error!

这几乎是相同的代码。为什么赋值操作这次不把 v 移到 v1 中呢?为了了解这一点,让我们再次看看堆栈中的内存布局:

在本例中,值是完全只存储在栈中。堆上没有东西可以拥有。这就是为什么允许通过 v 和 v1 访问是可以的 —— 因为它们是完全独立的拷贝。

这种不拥有其他资源并且可以按位复制的类型称为复制类型。它们实现了 Copy Trait[1]。目前所有基本类型,如整数、浮点数和字符都是 Copy 类型。默认情况下,struct/enum 不是 Copy,但你可以派生 Copy trait:

#[derive(Copy, Clone)] struct Point { x: i32, y: i32, }

#[derive(Copy, Clone)] enum SignedOrUnsignedInt { Signed(i32), Unsigned(u32), }

📒 : 需要在 #[derive()] 中同时使用 Clone,因为 Copy 是这样定义的: pub trait Copy: Clone {}

但是要使 #[derive(Copy, Clone)] 起作用,struct 或 enum 的所有成员必须可以 Copy。例如,下面代码就不起作用:

// error:the trait Copy may not be implemented for this type // because its nums field does not implement Copy #[derive(Copy, Clone)] struct Numbers { nums: Vec }

当然,你也可以手动实现 Copy 和 Clone:

struct Point { x: i32, y: i32, }

// marker trait implCopy for Point {}

implClone for Point { fn clone(&self) -> Point { *self } }

📒 : marker trait → 本身没有任何行为,但被用于给编译器提供某些保证。具体可以看这里[2]

但是一般来说,任何实现 Drop 的类型都不能被 Copy,因为 Drop 是由拥有一些资源的类型实现的。因为不能被简单地按位复制,但是 Copy 类型应该是可以被简单复制的。因此, Drop 和 Copy 不能很好地混合。

这就是关于 Copy 的全部内容。接下来是 Clone。

Clone

当一个值被移动时,Rust 会做一个浅拷贝;但是如果你想创建一个像 C++那样的深拷贝呢? 为了实现这一点,一个类型必须首先得实现 Clone Trait[3]。然后,为了能进行深复制,调用端代码应该执行 clone():

let v: Vec = Vec::new(); let v1 = v.clone(); // ok since Vec implements Clone println!("v's length is {}", v.len());//ok

clone() 调用后,内存布局如下:

由于深拷贝,v 和 v1 都可以自由独立地释放它们对应的堆缓冲区数据。

📒 : clone() 并不总是创建深拷贝。类型可以自由地以任何他们想要的方式实现 clone(),但在语义上它应该足够接近复制一个对象的含义。例如,Rc/Arc 会增加引用计数。

这就是关于 Clone 的所有内容。

总结

在这篇文章中,我深入分析了 Rust 中的 Move/Copy/Clone 的语义。同时在文章中试图捕捉与 C++ 中,在语义上的细微差别。 Rust 之所以优秀,是因为它有大量的默认行为。例如,Rust 中的赋值操作符要么移动值(转移所有权),要么进行简单的按位复制(浅拷贝)。

另一方面,在 C++ 中,看似无害的赋值操作可以隐藏大量的代码,而这些代码作为重载赋值运算符的一部分运行。在 Rust 中,这样的代码是公开的,因为程序员必须显式地调用 clone()。

有人可能会说,这两种语言做出了不同的权衡,但我喜欢 Rust 由于这些设计权衡而带来的额外安全保障。

References [1] Copy Trait: https://doc.rust-lang.org/std/marker/trait.Copy.html [2] 这里: https://doc.rust-lang.org/std/marker/index.html [3] Clone Trait: https://doc.rust-lang.org/std/clone/trait.Clone.html

评论区

写评论

还没有评论

1 共 0 条评论, 1 页