Rust:Programming Rust:所有权

来源:互联网 发布:数字青少年宫网络寄语 编辑:程序博客网 时间:2024/04/30 08:09

https://everstore.cn/a/oW-4NQbf9D

文章来自于:https://everstore.cn/t/encyclopedia,非常值得去看。

“我发现,甚至在我可以编译代码之前,Rust已经强迫我学习曾经那些在 C/C++中慢慢学会的好习惯。…我想强调,Rust不是几天内可以学习,以后再积累硬件/技术/好习惯那些东西的那种语言。你将被迫立即学习严格的安全性,起初可能会感到不舒服。然而,在我自己的经验中,它会让我觉得编译我的代码实际上是一种又上高楼的旅途。“

Mitchell Nordine

Rust做出以下一对对安全的系统编程语言至关重要的承诺:

你决定程序中每个值的生命周期。在你控制下的某个时刻,Rust会迅速释放属于某个值的内存和其他资源。
即使如此,你的程序永远不会使用指针指向释放后的对象。使用“悬挂指针”是C和C++中的常见错误:如果幸运的话,你的程序会崩溃。如果你不幸,你的程序会有一个安全漏洞。 Rust在编译时捕获这些错误。
C和C++遵守第一个承诺:你可以随时在动态分配的堆中的任何对象上调用free或delete。 但作为交换,第二个承诺被放在一边:保没有使用指针指向已释放的对象完全是你的责任。 有充分的实证证据表明,这是难以承担的责任:公共数据库中指针滥用一直是安全问题最常见的罪魁祸首。

许多语言使用垃圾收集来实现第二个承诺,当所有可达的指针都消失时自动释放对象。 但是作为交换,你将无法控制垃圾收集器何时释放对象。 一般来说,垃圾收集器是难以驾驭的野兽,并且要理解为什么内存没有如预期一样释放是一个挑战。如果你处理代表文件,网络连接或其他操作系统资源的对象,你将无法确信在你释放它们的时候它们真的会被释放,再者它们的底层资源与它们一起被清理,让人失望。

对于Rust来说,这些妥协是不可接受的:程序员应该控制值的生命周期,语言应该是安全的。 但这是一个已深入探究的语言设计领域。 没有一些根本的变化,你不能做出重大改进。

Rust以惊人的方式打破了僵局:通过限制程序如何使用指针。 本章将致力于解释这些限制以及它们的逻辑。 现在,总的来说你习惯使用的一些常见结构可能不符合规则,你需要寻找替代方案。 但是,这些限制的最终效果是能够为检查内存安全错误(悬挂指针,双重释放,使用未初始化的内存)的Rust的编译时检查,提供足够的秩序。 在运行时,你的指针是内存中的简单地址,就像在C和C++中一样。 不同的是,你的代码已经过验证可以安全地使用。

这些相同的规则也构成了Rust对安全并发编程的支持的基础。 使用Rust精心设计的线程原语,确保代码正确使用内存规则也可以避免数据竞争。 Rust程序中的错误不能导致一个线程损坏其他数据,在系统的不相关部分引入难以重现的故障。 多线程代码中固有的非确定性行为被孤立到设计来处理它们的功能——互斥体,消息通道,原子值等等,而不是出现在普通内存引用中。 C和C++中的多线程代码已声名狼藉,但Rust出色的挽回了颓势。

Rust的激进赌注——赌注越高获利更多的豪言,构成了这门语言的根本——即使有了这些限制,你会发现这门语言对于几乎每个任务来说都足够灵活,而且这个好处 ——消除广泛的内存管理和并发错误——将证明你需要对自己的风格进行调整。 本书的作者对Rust的正面评价正是因为我们在C和C++方面的丰富经验。 对我们来说,Rust是一门毋庸置疑值得学习的语言。

Rust的规则可能与你在其他编程语言中看到的规则不同。 在我们看来,学习如何使用它们并将其转化为你的优势是学习Rust的主要挑战。 在本章中,我们将首先通过展示与其他语言相同的底层问题来帮助理解Rust的规则。 然后,我们将详细说明Rust的规则。 最后,我们将讨论一些异常和最常见的异常。

所有权

如果你读了很多C或C ++代码,你可能会看过一个注释,说一些类的实例“拥有”它指向的其他一些对象。 这通常意味着对象的拥有者可以决定何时释放所拥有的对象:当所有者被销毁时,它的财产也会一起销毁。

例如,假设你编写以下C++代码:

std::string s = "frayed knot";

字符串s通常在内存中表示为:
这里写图片描述

这里,实际的std::string对象本身总是三个字长,包含指向堆分配缓冲区的指针; 缓冲区的总体容量(即,在字符串必须分配较大的缓冲区来保存文本之前,文本可以增长多大); 以及它现在拥有的文本的长度。 这些是std::string类的私有字段,字符串用户不可访问。

std::string拥有它的缓冲区:当程序销毁字符串时,字符串的析构函数释放缓冲区。 在过去,一些C++库在几个std::string值之间共享一个缓冲区,使用引用计数来决定何时释放缓冲区。 较新版本的C++规范有效排除了该表示; 所有现代C++库都使用这里所示的方法。在这些情况下通常能理解,尽管其他代码创建临时指针指向所拥有的内存是正确的,但是该代码有责任确保其指针在所有者决定销毁所有对象之前已经消失。 你可以创建一个指向std::string缓冲区中的字符的指针,但是当字符串被破坏时,你的指针变为无效,并且由你自己确定不再使用它。 所有者决定被拥有者的生命周期,其他人都必须尊重其决定。

Rust将这个原则从注释中拿出来,并将其用于语言中。 在Rust中,每个值都有一个所有者决定其生命周期。 当所有者被释放——dropped(Rust术语)时——所拥有的值也被删除。 这些规则旨在使你能够轻松地通过检查代码来查找任何给定值的生命周期,从而提供系统语言应该提供的生命周期的控制。

一个变量拥有它的值。 当控制离开声明变量的块时,该变量将被丢弃,因此它的值将随之被丢弃。 例如:

fn print_padovan() {    let mut padovan = vec![1,1,1];  // allocated here    for i in 3..10 {        let next = padovan[i-3] + padovan[i-2];        padovan.push(next);    }    println!("P(1..10) = {:?}", padovan);}                                   // dropped here

变量padovan的类型是std::vec::Vec,一个32位整数的向量。 在内存中,padovan的最终值将如下所示:
这里写图片描述

这非常类似于我们前面显示的C++std::string’,除了缓冲区中的元素是32位值不是字符。 请注意,持有指针,容量和长度的padovan直接存在于print_padovan`函数的栈帧中; 只有向量的缓冲区被分配在堆上。

与之前的字符串一样,向量拥有保存其元素的缓冲区。 当函数结束变量`padovan’超出范围时,程序会丢弃该向量。 而且由于矢量拥有它的缓冲区,这个缓冲区也随之而去。

Rust的Box类可以作为另一个所有权的例子。 一个Box是一个指向存储在堆上的T类型值的指针。 调用Box::new(v) 会分配一些堆空间,将值v移动到其中,并返回一个指向堆空间的Box。 由于Box拥有它指向的空间,当Box被摧毁时,它也释放了空间。

例如,你可以在堆中分配一个元组,如下所示:

{    let point = Box::new((0.625, 0.5));  // point allocated here    let label = format!("{:?}", point);  // label allocated here    assert_eq!(label, "(0.625, 0.5)");}                                        // both dropped here

当程序调用Box::new时,它会为堆中的两个f64值分配空间,将其参数(0.625,0.5)移动到该空间中,并返回一个指针。 当控制到达assert_eq!的调用时,栈帧如下所示:

这里写图片描述

栈帧本身保存变量point和label,每个变量都引用它拥有的堆分配。 当它们被丢弃时,他们拥有的分配与它们一起被释放。

就像变量拥有其值一样,结构体拥有其成员,元组,数组和向量拥有其元素。

struct Person { name: String, birth: i32 }let mut composers = Vec::new();composers.push(Person { name: "Palestrina".to_string(),                        birth: 1525 });composers.push(Person { name: "Dowland".to_string(),                        birth: 1563 });composers.push(Person { name: "Lully".to_string(),                        birth: 1632 });for composer in &composers {    println!("{}, born {}", composer.name, composer.birth);}

这里,composers是一个Vec,它是一个结构体的向量,每个结构都包含一个字符串和一个数字。在内存中,composers的最终值如下所示:
这里写图片描述

这里有很多所有权关系,但每一个都很简单:composers拥有一个向量; 矢量拥有它的元素,每个元素都是一个Person结构; 每个结构拥有其字段; 字符串字段拥有其文本。 当控制离开composers被声明的范围时,程序将丢弃其值,并将其整体移除。 如果图片中有其他类型的集合——一个HashMap,也许是一个BTreeSet——都是一样的。

当这里让我们回顾一下,考虑一下到目前为止所呈现的所有权关系的后果。 每个值都有一个所有者,使得决定何时删除它很容易。 但是,单个值可能拥有许多其他值:例如,矢量composers拥有其所有元素。 这些值可以依次拥有其他值:composers的每个元素都拥有一个字符串,它拥有其文本。

因此,所有者及其所拥有的值形成树:你的所有者是你的父母,你拥有的值是你的孩子。 而每个树的最终根是一个变量; 当该变量超出范围时,整个树就随之而去。 我们可以在composers的图表中看到这样一个所有权树:它不是搜索树数据结构或由DOM元素构成的HTML文档。 相反,我们有一个由混合类型构成的树,Rust的单一所有者规则禁止重新结合任何结构,这使得它可以构造比树更复杂模型。 Rust程序中的每个值都是某些树的一个成员,这些树根植于一些变量中。

Rust程序根本不显式删除值,C和C++程序会使用free和delete。 在Rust中删除值的方法是将其从所有权树中删除:通过离开变量的范围,或从向量中删除元素,或者这类事件。 在这一点上,Rust确保该值以及它拥有的一切被正确的丢弃。

在某种意义上,Rust没有其他语言强大:使用其他实用的编程语言,可以以任何你认为合适的方式构建彼此指向的对象的任意图形。 但是正是因为Rust不那么强大,在你的程序上执行的语言分析可以更强大。 Rust的安全保障是可能的,因为你的代码中可能出现的关系更可跟踪。 这是Rust先前提到的“激进赌注”的一部分:在实践中,Rust承若,解决问题时通常有足够的灵活性,至少能确定有几个完美的解决方案能符合语言的限制规范。

也就是说,我们迄今为止所讲的故事仍然太僵硬,在现实中举步维艰。 Rust以几种方式扩展了前景:

你可以将值从一个所有者移动到另一个。 这允许你构建,重新排列和销毁树。
标准库提供了引用计数的指针类型Rc和Arc,允许值在一些限制下有多个所有者。
你可以“借一个引用”到一个值; 引用是不是拥有者,生命周期有限的指针。
这些策略都为所有权模式提供了灵活性,同时都没有违背Rust的承诺。 我们将依次说明。

移动

在Rust中,对于大多数类型,如把值分配给变量,传递给函数或从函数返回的操作不会复制该值:它们移动它。 源将该值的所有权转交到目的地,并变为未初始化; 目的地现在控制该值的生命周期。 Rust 程序一次建立和拆除复杂结构一个值,一次移动一个。

你可能会惊讶,Rust改变了这种基本操作的意义; 赋值应该是早已定论的东西。 但是,如果你仔细观察不同语言处理赋值的方式,那么你会发现,它们之间存在很大的差异。 这种比较也使得Rust的选择的意义和后果更显而易见。

考虑以下Python代码:

s = ['udon', 'ramen', 'soba']t = su = s

每个Python对象都带有引用计数,跟踪当前引用的数量。 所以在赋值给s之后,程序的状态看起来就像这样(省略了一些字段):
这里写图片描述

由于只有`s’指向列表,所以列表的引用计数为1; 并且因为列表是指向字符串的唯一对象,它们的每个引用计数也是1。

当程序执行t和u的分配时会发生什么? Python通过使目标点与源指向相同的对象来实现分配,并增加对象的引用计数。 所以程序的最终状态是这样的:

这里写图片描述

Python将指针从 s 复制到t和 u,在Python中的赋值是便宜的,但是因为创建了对对象的新引用,我们必须维护引用计数 ,以便我们能知道什么时候可以释放值。

现在考虑类似的C++代码:

using namespace std;vector<string> s = { "udon", "ramen", "soba" };vector<string> t = s;vector<string> u = s;

s的原始值在内存中看起来像这样:

这里写图片描述

当程序将 s赋值给t和u会发生什么? 在C++中,分配一个std::vector’会生成一个矢量的副本;std::string`的行为类似。 所以当程序达到这段代码的末尾时,它实际上分配了三个向量和九个字符串:

这里写图片描述

根据所涉及的值,C++中赋值可以消耗无限量的内存和处理器时间。 然而,优点是程序容易决定何时释放所有这些内存:当变量超出范围时,这里分配的所有内容都将自动清除。

在某种意义上,C++和Python选择了相反的权衡:Python使得赋值成本低廉,付出引用计数的代价(在一般情况下是垃圾收集)。 C++保持所有内存明确的所有权,付出深度复制的代价。 C++程序员往往不太热衷于这个选择:深层复制可能是昂贵的,而且通常还有更多的实用替代方案。

那么在Rust里怎么处理类似的程序? 以下是代码:

let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];let t = s;let u = s;

像C和C++一样,Rust将简单的字符串文字(如udon)放在只读内存中,因此为了更清楚地与C++和Python示例进行比较,我们在这里调用to_string’来获取堆分配的String`值。

在执行s的初始化之后,由于Rust和C++对向量和字符串使用相似的表示,所以情况就像在C++中一样:

这里写图片描述

但是请记住,在Rust中,大多数类型的分配会将值从源移动到目的地,从而使源未初始化。所以在初始化t后,程序的内存如下所示:

这里写图片描述

这里发生了什么? 初始化let t = s;将向量的三个头字段从s’移动到t; 现在t`拥有向量。 向量的元素停留在它们原本的地方,字符串也没有发生任何事情。 每个价值仍然有一个所有者,虽然换了一个人。 这里没有引用计数被调整。 而编译器现在认为’s’未初始化。

那么当我们运行到初始化let u = s;会发生什么? 这将把未初始化的值s分配给u。 Rust谨慎地禁止使用未初始化的值,因此编译器会拒绝此代码,并显示以下错误:

error[E0382]: use of moved value: `s` --> ownership_double_move.rs:9:9  |8 |     let t = s;  |         - value moved here9 |     let u = s;  |         ^ value used here after move  |

像Python一样,Rust赋值很廉价:程序只需将向量的三个字头的头部从一个位置移动到另一个位置。 但是像C++一样,所有权一直是清楚的:程序不需要引用计数或垃圾收集来判断何时释放向量元素和字符串内容。

你付出的代价是当你想要副本时你需要明确地要求。 如果要达到与C++程序相同的状态,每个变量都保存一个独立的结构副本,你必须调用向量的clone方法,该方法执行向量及其元素的深层拷贝:

let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];let t = s.clone();let u = s.clone();

你还可以使用Rust的引用计数指针类型重新创建Python的行为。

更多移动操作

在迄今为止的例子中,我们已经演示了初始化,当它们进入let语句的范围为变量提供值。 赋值一个变量有些不同,因为如果你将一个值移动到一个已被初始化的变量中,那么Rust会删除变量的先前值。

例如:

let mut s = "Govinda".to_string();s = "Siddhartha".to_string(); // value "Govinda" dropped here

在这段代码中,当程序将字符串Siddhartha`分配给s时,其先前的值Govinda将首先丢弃。 但请考虑以下几点:

let mut s = "Govinda".to_string();let t = s;s = "Siddhartha".to_string(); // nothing is dropped here

这一次,t 从 s获取原字符串的所有权, 当我们给 s赋值时,s已变成未初始化了。 在这种情况下,不会删除任何字符串。

我们在这里的示例中使用了初始化和分配,因为它们很简单,但Rust应用将语义几乎移植到任何值中。 将参数传递给函数将所有权移动到函数的参数中; 从函数返回值将所有权移动到调用者。 构建元组将值移动到元组中。 等等。

你可以在上一节中提供的示例中更好地了解真实情况。 例如,当我们在构建我们的作曲家向量时,我们编写了:

struct Person { name: String, birth: i32 }let mut composers = Vec::new();composers.push(Person { name: "Palestrina".to_string(),                        birth: 1525 });

此代码显示了几个发生移动的地方,超出了初始化和分配:

从函数返回值。 调用Vec::new()构造一个新的向量,并返回,不是一个指向向量的指针而是向量本身:它的所有权从Vec::new移动到变量composers。 类似地,to_string调用返回一个新的String实例。
构建新值。 新的Person结构的name字段用to_string的返回值初始化。 该结构占用该字符串的所有权。
将值传递给函数。 整个Person结构,而不仅仅是一个指针,被传递给向量的push方法,它将它移动到结构的末尾。 该向量占用该Person的所有权,因此也成为名称name的间接所有者。
像这样移动值可能听起来效率不高,但要注意两件事情。 首先,移动总是应用于值本身,而不是它们拥有的堆存储。 对于向量和字符串,值本身仅是三字头, 潜在的大型元素数组和文本缓冲区位于堆中。 第二,Rust编译器的代码生成非常擅长看穿所有这些动作; 在实践中,机器代码通常直接在其所在的位置存储值。

移动和控制流程

上面的例子都有非常简单的控制流程; 移动如何与更复杂的代码进行交互? 一般的原则是,如果一个变量有可能将其值移开,并且一直没有确切的赋予一个新的值,那么它被认为是未初始化的。 例如,如果在评估if表达式的条件之后变量仍然有一个值,那么我们可以在两个分支中使用它:

let x = vec![10, 20, 30];if c {    f(x); // ... okay to move from x here} else {    g(x); // ... and okay to also move from x here}h(x) // bad: x is uninitialized here if either path uses it

由于类似的原因,循环中禁止变量的移动:

let x = vec![10, 20, 30];while f() {    g(x); // bad: x would be moved in first iteration,          // uninitialized in second}

也就是说,除非我们在下一次迭代中确切给它一个新的价值:

let mut x = vec![10, 20, 30];while f() {    g(x);           // move from x    x = h();        // give x a fresh value}e(x);

移动和编入索引的内容

我们已经提到,一个移动将其源未初始化,因为目的地拥有该值的所有权。 但不是每一种值的所有者都准备好变成未初始化。 例如,考虑以下代码:

// Build a vector of the strings "101", "102", ... "105"let mut v = Vec::new();for i in 101 .. 106 {    v.push(i.to_string());}// Pull out random elements from the vector.let third = v[2];let fifth = v[4];

为了这样可行,Rust需要以某种方式记住向量的第三个和第五个元素已变成未初始化,并跟踪该信息,直到向量被丢弃。 在最普遍的情况下,向量将需要携带额外的信息,以指示哪些元素是活的,哪些元素已经初始化。 这显然不是系统编程语言的正确行为; 一个向量应该只是一个向量。 实际上,Rust以错误拒绝上面的代码:

error[E0507]: cannot move out of indexed content  --> ownership_move_out_of_vector.rs:14:17   |14 |     let third = v[2];   |                 ^^^^   |                 |   |                 help: consider using a reference instead `&v[2]`   |                 cannot move out of indexed content

对于“第五”个,它也有类似的错误提示。 在错误消息中,Rust建议使用引用,以在不移动的前提下访问它。 这通常是你想要的。 但是,如果你真的想要将一个元素从一个向量中移出呢? 你需要找到一种方法,以符合类型限制的方式执行此操作。 这里有三种可能性:

// Build a vector of the strings "101", "102", ... "105"let mut v = Vec::new();for i in 101 .. 106 {    v.push(i.to_string());}// 1. Pop a value off the end of the vector:let fifth = v.pop().unwrap();assert_eq!(fifth, "105");// 2. Move a value out of the middle of the vector, and move the last// element into its spot:let second = v.swap_remove(1);assert_eq!(second, "102");// 3. Swap in another value for the one we're taking out:let third = std::mem::replace(&mut v[2], "substitute".to_string());assert_eq!(third, "103");// Let's see what's left of our vector.assert_eq!(v, vec!["101", "104", "substitute"]);

这些方法都将元素移出向量,但是以保留向量完全填充的状态(如果可能更小)的方式。

像Vec这样的集合类型通常还提供了一种在循环中消耗其所有元素的方法:

let v = vec!["liberté".to_string(),             "égalité".to_string(),             "fraternité".to_string()];for mut s in v {    s.push('!');    println!("{}", s);}

当我们将向量直接传递给循环时,如for … in v中,将向量移出v,使v’未初始化。for循环的内部机制拥有向量的所有权,并将其分解成其元素。 在每次迭代时,循环将另一个元素移动到变量s。 由于s`现在拥有这个字符串,所以在打印之前我们可以在循环体中进行修改。 而且由于向量本身对于代码不再可见,所以部分为空的状态在循环中没什么可以观察。

如果你发现自己需要从编译器无法跟踪的所有者中移出值,则可以考虑将所有者的类型更改为可以动态跟踪是否具有值。 例如,前面的例子的一个变体:

struct Person { name: Option<String>, birth: i32 }let mut composers = Vec::new();composers.push(Person { name: Some("Palestrina".to_string()),                        birth: 1525 });

你不能这样做:

let first_name = composers[0].name;

这将只是引出前面显示过相同的cannot move out of indexed content错误。 但是,因为你将name字段的类型从String更改为Option,这意味着None是字段的合法值,所以这样做是可行的:

let first_name = std::mem::replace(&mut composers[0].name, None);assert_eq!(first_name, Some("Palestrina".to_string()));assert_eq!(composers[0].name, None);

replace调用移出composers [0] .name的值,把None留在它的位置,并将原始值的所有权传递给它的调用者。 事实上,以这种方式使用Option是非常普遍的,为了这个目的,该类型提供了一个take方法。 你可以把上面的操作更清晰地写成:

let first_name = composers[0].name.take();

对take的这个调用与上面对replace的调用相同。

Copy 类型:异常移动

我们迄今为止演示的值被移动的示例涉及到可能使用大量内存并且复制成本高昂的向量,字符串和其他类型。 移动保持这种类型清晰的所有权和廉价的赋值。 但是对于像整数或字符这样更简单的类型,这种仔细处理并不是必需的。

与分配一个String比较,当我们分配一个’i32`值时内存中会发生什么:

let str1 = "somnambulance".to_string();let str2 = str1;let num1: i32 = 36;let num2 = num1;

运行此代码后,内存如下所示:

这里写图片描述

与前面的向量一样,赋值将str1移动到str2,这样我们最终不会有两个负责释放相同缓冲区的字符串。 但是,num1和num2的情况是不同的。 i32只是内存中的一种位的模式; 它不拥有任何堆资源,或者真正依赖除字节以外的任何东西。 当我们把它的位移动到num2时,我们完成了一个完全独立的`num1’的拷贝。

移动一个值其源的会变得未初始化。 但是,它把str1当做无值来对待的基本目的,对num1是无意义的; 继续使用不会造成伤害。 移动的优点在这里不适用,它是不方便的。

以前我们很小心地说,大多数类型被移动; 现在这个是例外,Rust指定这些类型为Copy。 分配Copy类型的值会复制该值,而不是移动该值。 分配源保持初始化和可用性,具有与分配前相同的值。 将Copy类型传递给函数和构造函数的行为类似。

标准的Copy类型包括所有的机器整数和浮点数,char和bool类型和其他几个。 Copy类型的元组或固定大小的数组本身就是一个Copy类型。

只有一个简单的位到位复制足够的类型可以是 Copy。 如上所述,String不是一个Copy类型,因为它拥有一个堆分配的缓冲区。 由于类似的原因,Box不是Copy; 它拥有堆分配的对象。 File类型,表示操作系统文件句柄,不是Copy; 复制这样的值将需要向操作系统询问另一个文件句柄。 同样,MutexGuard类型,代表一个锁定的互斥体,不是Copy:这个类型的复制根本没有意义,因为一次只有一个线程可以持有互斥体。

作为一个经验法则,当一个值被丢弃任何需要做某些特殊操作的类型不能是Copy。 Vec需要释放其元素; 一个File需要关闭它的文件句柄; MutexGuard需要解锁其互斥体。 这种类型的位对位的复制将使谁负责原资源变得不清楚。

你自己定义的类型怎么样? 默认情况下,struct和enum类型不是Copy:

struct Label { number: u32 }fn print(l: Label) { println!("STAMP: {}", l.number); }let l = Label { number: 3 };print(l);println!("My label number is: {}", l.number);

这不会编译; 生锈抱怨:

error[E0382]: use of moved value: `l.number`  --> ownership_struct.rs:12:40   |11 |     print(l);   |           - value moved here12 |     println!("My label number is: {}", l.number);   |                                        ^^^^^^^^ value used here after move   |   = note: move occurs because `l` has type `main::Label`, which does not           implement the `Copy` trait

由于Label不是Copy,所以把它传给print把值的所有权移到print函数,然后在返回之前丢弃它。 但这是愚蠢的 一个Label只不过是一个’i32’的伪装。 没有理由把l传给print应该移动值。

#[derive(Copy, Clone)]struct Label { number: u32 }

有了这个变化,上面的代码可以编译。 但是,如果我们尝试这样一个字段不全为Copy的类型,则不起作用。 编译以下代码:

#[derive(Copy, Clone)]struct StringLabel { name: String }

引发错误:

error[E0204]: the trait `Copy` may not be implemented for this type --> ownership_string_label.rs:7:10  |7 | #[derive(Copy, Clone)]  |          ^^^^8 | struct StringLabel { name: String }  |                      ------------ this field does not implement `Copy`

为什么用户定义的类型不自动Copy,假设它们符合条件? 一个类型是否为Copy对它的使用有很大的影响:Copy类型更灵活,因为分配和相关操作不会使源未初始化。 但是对于一个类型的实现者,情况恰恰相反:它们可以包含的类型中Copy类型非常限制,而非Copy类型可以使用堆分配和拥有其他种类的资源。 因此,让一个类型Copy代表了对实现者施加了一道限制:如果有必要将其更改为非Copy,那么使用它的大部分代码可能需要进行修改。

虽然C++允许你重载分配运算符并定义专门的复制和移动构造函数,但Rust不允许进行这种定制。 在Rust中,每一个移动都是一个字节对字节,浅拷贝,使源未初始化。 复制是相同的,除了源保持初始化。 这意味着C++类可以提供Rust不能提供的方便的接口,C++的这种接口的代码普遍隐含地调整引用计数,提供昂贵的复制以供以后使用,或使用其他复杂的实现技巧。

但是,这种灵活性对C++作为一种语言的影响是使基本操作像分配,传递参数和从函数返回的值不太可预测。 例如,本章前面我们展示了在C++中将一个变量分配给另一个变量如何可能需要任意数量的内存和处理器时间。 Rust的原则之一是程序员应该明白成本。 基本操作必须保持简单。 潜在昂贵的操作应该是明确的,就像前面例子中对clone的调用一样,它们可以制作向量的深层副本和它们包含的字符串。

Rc和Arc:共享的所有权

虽然典型的Rust代码中的大多数值具有唯一所有者,但在某些情况下,很难找到单个具有所需生命周期的所有者的每个值; 你只是想让这个值活着,直到每个人都使用完它。 对于这些情况,Rust提供了引用计数的指针类型Rc和Arc。 正如你对Rust的期望一样,它们的使用是完全安全的:你不能忘记调整引用计数,或创建其他Rust不能跟踪的指针,或者被任何其他C++引用计数类的问题绊倒。

Rc和Arc类型非常相似; 它们之间的唯一区别是Arc可以直接在线程之间共享——名称Arc是Atomic Reference Count的缩写——而简单的Rc使用更快的非线程安全的代码来更新 引用计数。 如果你不需要在线程之间共享指针,则没有理由支付Arc的性能损失,因此你应该使用Rc; Rust会阻止你意外地穿过线程边界。 这两种类型是相同的,所以在本节的其余部分,我们将只谈Rc。

在本章前面,我们展示了Python如何使用引用计数来管理其值的生命周期。 你可以使用Rc在Rust中获得类似的效果。 请考虑以下代码:

use std::rc::Rc;// Rust can infer all these types; written out for claritylet s: Rc<String> = Rc::new("shirataki".to_string());let t: Rc<String> = s.clone();let u: Rc<String> = s.clone();

对于任何类型的T,Rc是一个附加了引用计数指向堆分配的T的指针。 克隆Rc值不会复制T; 相反,它只是创建另一个指向它的指针,并增加引用计数。 所以以上代码在内存中产生以下情况:
这里写图片描述

三个Rc指针都指向同一个内存块,它保存String的引用计数和空间。 普通的所有权规则适用于Rc指针本身,当最后一个现存的Rc被删除时,Rust也会删除String。

你可以直接在Rc上使用任何String通常的方法。

assert!(s.contains("shira"));assert_eq!(t.find("taki"), Some(5));println!("{} are quite chewy, almost bouncy, but lack flavor", u);

Rc指针所拥有的值是不可变的。 如果你尝试在字符串的末尾添加一些文本:

s.push_str(" noodles");

Rust会拒绝:

error: cannot borrow immutable borrowed content as mutable  --> ownership_rc_mutability.rs:12:5   |12 |     s.push_str(" noodles");   |     ^ cannot borrow as mutable

Rust的内存和线程安全保障取决于确保没有任何值被共享和可变。 Rust假定Rc指针的引用通常可以共享,因此它不能是可变的。 我们解释为什么这个限制在[第5章](ch05.html#参考)中很重要。

使用引用计数来管理内存的一个众所周知的问题是,如果有两个指向彼此的引用计数值,则每个引用计数值将保持高于零的对另一个的引用计数,因此值将永远不会被释放:

这里写图片描述

这样可以在Rust中泄漏值,但这种情况很少见。 你不能创建一个循环,而不会在某种程度上使较旧的值指向较新的值。 这显然需要较老的值可变。 由于Rc指针保持其指示不可变,所以通常不可能创建一个循环。 然而,Rust确实提供了创建不变值的可变部分的方法; 这被称为内部可变性,我们将在“内部可变性”中详细介绍它。 如果将这些技术与Rc指针相结合,你可以创建一个循环和内存泄漏。

有时可以通过使用“弱指针”,std::rc::Weak来代替某些链接来避免产生Rc指针的循环。 但是,我们不会在这里覆盖此主题; 有关详细信息,请参阅标准库的文档。

移动和引用计数指针是放松所有权树刚度的两种方式。 在下一章中,我们来看一下第三种方法:借用对值的引用。 一旦你学会了所有权和借用,就爬上Rust学习曲线最陡峭的部分,你将可以充分利用Rust的独特优势。