乐趣区

关于rust:带你了解-Rust-中的move-copy-clone

move, copy, clone

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


本文对 move, copy, clone 不做中文翻译,放弃在 Rust 中的滋味,翻译了就没哪味。

介绍

挪动和复制是 Rust 中的基本概念。对于来自 Ruby、Python 或 C# 等具备垃圾回收性能语言的开发者来说,这些概念可能是齐全生疏的。尽管这些术语在 c ++ 中是存在的,但它们在 Rust 中的含意略有不同。在这篇文章中,我将解释在 Rust 中 move、copy 和 clone 的含意。就让咱们一探到底吧。

Move

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

let v: Vec<i32> = 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<i32> = Vec::new();
let v1 = v;
println!("v's length is {}", v.len());   // error: borrow of moved value: `v`

这种所有权的扭转是无益的,因为如果同时容许通过 v 和 v1 拜访缓冲区数据,那么你将会失去两个栈对象指向同一个堆缓冲区:

在这种状况下,哪个对象有权开释缓冲区?这点并不分明,而 Rust 从基本就避免了这种状况的呈现。(即:赋值 → 栈对象拷贝,同时转移所有权, 保障一个对象在同一时间只能有一个所有者 )

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

let v: Vec<i32> = 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<i32>) ->Vec<i32> {println!("v1's length is {}", v1.len());
    v1     // v1 is moved out of the function
}

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

struct Numbers {nums:Vec<i32>}
let v: Vec<i32> = 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<i32> = 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。目前所有根本类型,如整数、浮点数和字符都是 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<i32>}

当然,你也能够手动实现 Copy 和 Clone:

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

// marker trait
implCopy for Point {}

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

📒 :
marker trait → 自身没有任何行为,但被用于给编译器提供某些保障。具体能够看这里

然而一般来说, 任何实现 Drop 的类型都不能被 Copy,因为 Drop 是由领有一些资源的类型实现的 。因为不能被简略地按位复制,然而 Copy 类型应该是能够被简略复制的。因而,Drop 和 Copy 不能很好地混合。

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

Clone

当一个值被挪动时,Rust 会做一个浅拷贝;然而如果你想创立一个像 C ++ 那样的深拷贝呢?

为了实现这一点,一个类型必须首先得实现 Clone Trait。而后,为了能进行深复制,调用端代码应该执行 clone()

let v: Vec<i32> = 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 因为这些设计衡量而带来的额定平安保障。

退出移动版