Libraries cannot provide new inabilities.
—Mark Miller
我们已经看到的所有指针类型 - 简单的Box 堆指针,以及String和Vec值内部的指针 - 都拥有指针:当所有者被删除时,指示对象依赖它。 Rust还有一些称为引用的非归属指针类型,它们对所指对象的生命周期没有影响。

事实上,它恰恰相反:引用必须永远不会超过它们的指示物。您必须在代码中明确指出,任何引用都不可能超过它指向的值。为了强调这一点,Rust指的是创建对某些值的引用作为借用值:您借入的内容,最终必须返回其所有者。

如果你在阅读“你必须在代码中表现出来”这句话时感到有点怀疑,那么你就是一个出色的公司。引用本身并不特别引人注目,它们只是地址。但保证他们安全的规则对Rust来说是新颖的;在研究语言之外,你以前不会看到类似的东西。虽然这些规则是Rust的一部分,需要最大的努力来掌握,但它们预防的经典,绝对日常错误的广度是令人惊讶的,它们对多线程编程的影响正在释放。这又是鲁斯特的激进赌注。

举个例子,让我们假设我们要建立一个凶悍的文艺复兴时期艺术家的桌子以及他们所知道的作品。 Rust的标准库包含一个哈希表类型,因此我们可以像这样定义我们的类型:

use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>;

换句话说,这是一个哈希表,它将String值映射到Vec 值,将艺术家的名称带到其作品名称列表中。您可以使用for循环遍历HashMap的条目,因此我们可以编写一个函数来打印一个Table进行调试:

fn show(table: Table) {for (artist, works) in table {println!("works by {}:", artist);for work in works {println!(" {}", work);}}
}

构建和打印表格非常简单:

fn main() {let mut table = Table::new();table.insert("Gesualdo".to_string(),vec!["many madrigals".to_string(),"Tenebrae Responsoria".to_string()]);table.insert("Caravaggio".to_string(),vec!["The Musicians".to_string(),"The Calling of St. Matthew".to_string()]);table.insert("Cellini".to_string(),vec!["Perseus with the head of Medusa".to_string(),"a salt cellar".to_string()]);show(table);
}

一切正常:

$ cargo runRunning `/home/jimb/rust/book/fragments/target/debug/fragments`
works by Gesualdo:Tenebrae Responsoriamany madrigals
works by Cellini:Perseus with the head of Medusaa salt cellar
works by Caravaggio:The MusiciansThe Calling of St. Matthew
$

但是如果你已经阅读了前一章关于移动的章节,那么show的这个定义应该引出一些问题。特别是,HashMap不是Copy-它不能,因为它拥有一个动态分配的表。因此,当程序调用show(table)时,整个结构将移动到函数,使变量表保持未初始化状态。如果调用代码现在尝试使用表,它将遇到麻烦:

...
show(table);
assert_eq!(table["Gesualdo"][0], "many madrigals");

Rust抱怨table不再可用:

error[E0382]: use of moved value: `table`--> references_show_moves_table.rs:29:16|
28 | show(table);| ----- value moved here
29 | assert_eq!(table["Gesualdo"][0], "many madrigals");| ^^^^^ value used here after move|= note: move occurs because `table` has type `HashMap<String, Vec<String>>`,which does not implement the `Copy` trait

实际上,如果我们查看show的定义,外部for循环将获取哈希表的所有权并完全使用它;并且内部for循环对每个向量执行相同的操作。 (我们之前在“liberté,égalité,fraternité”示例中看到了这种行为。)由于移动语义,我们只是通过尝试将其打印出来完全破坏了整个结构。谢谢,Rust!

处理此问题的正确方法是使用引用。通过引用,您可以在不影响其所有权的情况下访问值。参考文献有两种:
•共享引用允许您读取但不能修改其引用。但是,您可以根据需要一次对特定值进行尽可能多的共享引用。表达式&e产生对e值的共享引用;如果e的类型为T,那么&e的类型为&T,发音为“ref T”。共享引用是复制。

•如果您对值有可变引用,则可以同时读取和修改该值。但是,您可能没有任何其他任何类型的引用同时激活该值。表达式&mut e产生对e值的可变引用;你把它的类型写成&mut T,发音为“ref mute T”。可变引用不是复制。

您可以将共享和可变引用之间的区别视为在编译时强制执行多个读者或单个编写器规则的方法。实际上,这条规则不仅适用于参考文献;它也涵盖了借来的价值所有者。只要存在对值的共享引用,即使其所有者也可以修改它;该值被锁定。当show正在使用它时,没有人可以修改表。类似地,如果存在对值的可变引用,则它具有对该值的独占访问权;在可变参考消失之前,你根本不能使用所有者。保持共享和变异完全分离对记忆安全至关重要,原因我们将在本章后面介绍。

我们示例中的打印功能不需要修改表,只需读取它即可
内容。因此调用者应该能够将它传递给表的共享引用,如下所示:

show(&table);

引用是非常量指针,因此表变量仍然是整个结构的所有者;秀刚刚借了一下。当然,我们需要调整show的定义来匹配,但你必须仔细观察才能看出差异:

inition of show to match, but you’ll have to look closely to see the difference:
fn show(table: &Table) {for (artist, works) in table {println!("works by {}:", artist);for work in works {println!(" {}", work);}}
}

show的参数表的类型已从Table更改为&Table:而不是按值传递表(因此将所有权移动到函数中),我们现在传递共享引用。这是唯一的文字变化。但是,当我们在身体中工作时,这是如何发挥作用的呢?

虽然我们的原始外部for循环获得了HashMap的所有权并使用了它,但在我们的新版本中它接收了对HashMap的共享引用。迭代对HashMap的共享引用被定义为生成对每个条目的键和值的共享引用:artist已从String更改为&String,并且从Vec 工作到&Vec 。

内环类似地改变。迭代对向量的共享引用被定义为生成对其元素的共享引用,因此工作现在是&String。在此功能的任何地方都没有所有权转手;它只是传递非公开的引用。

现在,如果我们想编写一个函数来按字母顺序分析每个艺术家的作品,那么共享引用是不够的,因为共享引用不允许修改。相反,排序函数需要对表进行可变引用:

fn sort_works(table: &mut Table) {for (_artist, works) in table {works.sort();}
}

我们需要传递一个:

sort_works(&mut table);

这种可变的借用赋予sort_works读取和修改结构的能力,这是矢量排序方法所要求的。

当我们以一种将值的所有权移动到函数的方式将值传递给函数时,我们说我们已经通过值传递了它。如果我们通过函数传递对值的引用,我们说我们通过引用传递了值。例如,我们修改了show函数,方法是将其更改为通过引用而不是按值接受表。许多语言都有这种区别,但在Rust中尤为重要,因为它阐明了所有权如何受到影响。

引用为值

前面的示例显示了引用的一个非常典型的用法:允许函数访问或操作结构而不占用所有权。但是引用比这更灵活,所以让我们看一些例子来更详细地了解正在发生的事情。

Rust参考文献与C ++参考文献

如果您熟悉C ++中的引用,它们确实与Rust引用有一些共同之处。最重要的是,它们都只是机器级别的地址。但在实践中,Rust的参考文献有着截然不同的感觉。

在C ++中,引用是通过转换隐式创建的,并且也隐式地解引用:

o:
// C++ code!
int x = 10;
int &r = x; // initialization creates reference implicitly
assert(r == 10); // implicitly dereference r to see x's value
r = 20; // stores 20 in x, r itself still points to x

在Rust中,使用&运算符显式创建引用,并使用*运算符显式解除引用:

// Back to Rust code from this point onward.
let x = 10;
let r = &x; // &x is a shared reference to x
assert!(*r == 10); // explicitly dereference r

要创建可变引用,请使用&mut运算符:

let mut y = 32;
let m = &mut y; // &mut y is a mutable reference to y
*m += 32; // explicitly dereference m to set y's value
assert!(*m == 64); // and to see y's new value

但你可能还记得,当我们
使用show函数来获取艺术家的表格而不是值,我们从来不必使用*运算符。这是为什么?
由于引用在Rust中如此广泛使用,所以。如果需要,运算符隐式取消引用其左操作数:

struct Anime { name: &'static str, bechdel_pass: bool };
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true };
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");
// Equivalent to the above, but with the dereference written out:
assert_eq!((*anime_ref).name, "Aria: The Animation");

println!() show函数中使用的宏扩展为使用的代码。运算符,因此它也利用了这种隐式解引用。的。如果方法调用需要,运算符也可以隐式地借用对其左操作数的引用。例如,Vec的sort方法对向量采用可变引用,因此这里显示的两个调用是等效的:

let mut v = vec![1973, 1968];
v.sort(); // implicitly borrows a mutable reference to v
(&mut v).sort(); // equivalent; much uglier

简而言之,C ++在引用和左值之间隐式转换(即引用内存中位置的表达式),这些转换出现在任何需要的位置,在Rust中,您使用&和*运算符来创建和跟踪引用,除了。运算符,隐含地借用和解除引用。

分配参考

分配给Rust引用使其指向一个新值:

ssigning to a Rust reference makes it point at a new value:
let x = 10;
let y = 20;
let mut r = &x;
if b { r = &y; }
assert!(*r == 10 || *r == 20);

参考r最初指向x。但如果b为真,则代码将其指向y,如图5-1所示。

这与C ++非常不同,C ++分配给引用将值存储在其引用中。没有办法将C ++引用指向除初始化之外的位置。

引用的引用

Rust允许引用引用:

struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;

(为了清楚起见,我们已经写出了参考类型,但你可以省略它们;这里没有任何东西,Rust无法推断自己。)。运算符遵循尽可能多的引用来查找其目标:

assert_eq!(rrr.y, 729);

在内存中,引用的排列如图5-2所示

因此,由rrr类型引导的表达式rrr.y实际上遍历三个引用以在获取其y字段之前到达Point。

比较引用

像 。运算符,Rust的比较运算符“透视”任意数量的引用,只要两个操作数具有相同的类型:

let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = &rx;
let rry = &ry;
assert!(rrx <= rry);
assert!(rrx == rry);

这里的最终断言成功,即使rrx和rry指向不同的值(即rx和ry),因为==运算符跟随所有引用并对其最终目标x和y执行比较。这几乎总是你想要的行为,特别是在编写泛型函数时。如果你真的想知道两个引用是否指向同一个内存,你可以使用std :: ptr :: eq,它们将它们作为地址进行比较:

assert!(rx == ry); // their referents are equal
assert!(!std::ptr::eq(rx, ry)); // but occupy different addresses

引用绝不是空的

Rust引用永远不会为null。 C的NULL或C ++的nullptr没有类似的东西;引用没有默认初始值(在初始化之前不能使用任何变量,无论其类型如何);和Rust不会将整数转换为引用(在不安全代码之外),因此您无法将零转换为引用。

C和C ++代码通常使用空指针来指示缺少值:例如,malloc函数或者返回指向新内存块的指针,如果没有足够的可用内存来满足请求,则返回nullptr。在Rust中,如果您需要的值是对某事物的引用,请使用类型Option <&T>。

在机器级别,Rust表示None作为空指针,而Some(r),其中r是a&T值,作为非零地址,因此Option <&T>与C或C ++中的可空指针一样高效,甚至虽然它更安全:它的类型要求你在使用之前检查它是否为None。

借用对任意表达式的引用

虽然C和C ++只允许将&运算符应用于某些类型的表达式,但Rust允许您借用对任何类型表达式的值的引用:

fn factorial(n: usize) -> usize {(1..n+1).fold(1, |a, b| a * b)
}
let r = &factorial(6);
assert_eq!(r + &1009, 1729);

在这种情况下,Rust只是创建一个匿名变量来保存表达式的值,并使引用指向该值。这个匿名变量的生命周期取决于你对引用的处理方式:

•如果您立即在let语句中为变量分配引用(或使其成为正在分配的某个结构或数组的一部分),那么只要变量let初始化,Rust就会使匿名变量生效。在前面的例子中,Rust会为r的指示对象执行此操作。

•否则,匿名变量将存在于封闭语句的末尾。在我们的示例中,为保持1009而创建的匿名变量仅持续到assert_eq的末尾!声明。

如果您习惯使用C或C ++,这可能听起来容易出错。但请记住,Rust永远不会让你编写会产生悬空引用的代码。如果引用可以在匿名变量的生命周期之外使用,Rust将始终在编译时向您报告问题。然后,您可以修复代码,以使指定变量保持适当的生命周期。

对切​​片和特征对象的引用

到目前为止我们展示的参考文献都是简单的地址。但是,Rust还包括两种胖指针,带有某些值的地址的双字值,以及使用该值所需的一些其他信息。

对切​​片的引用是胖指针,其携带切片的起始地址及其长度。我们在第3章中详细描述了切片。

Rust的另一种胖指针是一个特征对象,它是对实现特定特征的值的引用。特征对象携带一个值的地址和一个指向适合该值的特征实现的指针,用于调用特征的方法。我们将在第238页的“特征对象”中详细介绍特征对象。

除了携带这些额外的数据之外,切片和特征对象引用的行为就像我们在本章中到目前为止所展示的其他类型的引用一样:它们不拥有它们的引用;他们不被允许超过他们的指称;它们可能是可变的或共享的;等等。

引用安全

正如我们到目前为止所呈现的那样,引用看起来非常像C或C ++中的普通指针。但那些是不安全的; Rust如何控制其引用?可能最好的方法是看到实际的规则是试图打破它们。我们将从最简单的示例开始,然后添加有趣的复杂功能并解释它们是如何工作的。

借用局部变量

这是一个非常明显的案例。您不能借用对局部变量的引用并将其从变量的范围中取出:

{let r;{let x = 1;r = &x;}assert_eq!(*r, 1); // bad: reads memory `x` used to occupy
}

Rust编译器拒绝此程序,并带有详细的错误消息:

error: `x` does not live long enough--> references_dangling.rs:8:5|
7 | r = &x;| - borrow occurs here
8 | }| ^ `x` dropped here while still borrowed
9 | assert_eq!(*r, 1); // bad: reads memory `x` used to occupy
10 | }| - borrowed value needs to live until here

Rust的抱怨是x只存在于内部块的末尾,而引用仍然存在,直到外部块结束,使其成为一个悬垂的指针,这是一个禁止的。

虽然对于一个人类读者来说很明显这个程序被打破了,但值得看看Rust自己如何得出这个结论。即使这个简单的例子也显示了Rust用来检查更复杂代码的逻辑工具。

Rust尝试在程序中为每个引用类型分配一个生命周期,该生命周期满足其使用方式所施加的约束。生命周期是程序的一部分,可以安全地使用引用:词法块,语句,表达式,某些变量的范围等。生命周期完全是Rust的编译时想象力。在运行时,引用只不过是一个地址;它的生命周期是其类型的一部分,没有运行时表示。在这个例子中,有三个生命周期我们需要解决它们的关系。变量r和x每个都有一个生命周期,从它们被初始化的点延伸到它们超出范围的点。第三个生命周期是引用类型的生命周期:我们借用到&x的引用类型,并存储在r中。

这里的一个约束看起来非常明显:如果你有一个变量x,那么对x的引用不得超过x本身,如图5-3所示。

除了x超出范围之外,引用将是一个悬空指针。我们说变量的生命周期必须包含或包含从中借用的引用的生命周期。

这是另一种约束:如果将引用存储在变量r中,则引用的类型必须适用于变量的整个生命周期,从初始化到它超出范围的点,如图5所示。 -4。

如果引用不能至少与变量一样长,那么在某个时刻r将是一个悬空指针。我们说引用的生命周期必须包含或包含变量。

第一种约束限制了参考生命周期的大小,而第二种约束限制了参考的生命周期。 Rust只是试图找到满足所有这些约束的每个引用的生命周期。但是,在我们的示例中,没有这样的生命周期,如图5-5所示。

现在让我们考虑一下事情确实有效的另一个例子。我们有相同的约束条件:引用的生命周期必须由x包含,但完全包含r。但由于r的寿命现在较小,因此有一个寿命可以满足约束条件,如图5-6所示。

当您借用对某些较大数据结构的某些部分的引用时,这些规则以自然的方式应用,例如向量的元素:

let v = vec![1, 2, 3];
let r = &v[1];

由于v拥有拥有其元素的向量,因此v的生存期必须包含参考类型&v [1]的生命周期。同样,如果在某些数据结构中存储引用,则其生命周期必须包含数据结构的生命周期。如果你构建了一个引用向量,比如说,所有这些引用都必须包含拥有向量的变量的生命周期。

这是Rust用于所有代码的过程的本质。在图像数据结构和函数调用中引入了更多的语言特性,比如引入了新的约束条件,但原理仍然是相同的:首先,理解程序使用引用的方式所产生的约束;然后,找到满足他们的生命。这与C和C ++程序员强加于自己的过程没有什么不同;区别在于Rust了解规则并强制执行。

接收引用作为参数

当我们传递对函数的引用时,Rust如何确保函数安全地使用它?假设我们有一个函数f,它接受一个引用并将它存储在一个全局变量中。我们需要对此进行一些修改,但这是第一次修改:

// This code has several problems, and doesn't compile.
static mut STASH: &i32;
fn f(p: &i32) { STASH = p; }

Rust相当于一个全局变量称为静态:它是一个在程序启动时创建并持续到终止的值。 (像任何其他声明一样,Rust的模块系统控制静态可见的位置,因此它们在其生命周期中只是“全局”,而不是它们的可见性。)我们将在第8章介绍静态,但是现在我们只需要说几句代码刚刚显示的规则不遵循:

•必须初始化每个静态。

•可变静态本质上不是线程安全的(毕竟,任何线程都可以随时访问静态),甚至在单线程程序中,它们也可能成为其他类型的重入问题的牺牲品。由于这些原因,您可以仅在不安全的块中访问可变静态。在这个例子中,我们并不关心那些特定的问题,所以我们只是抛出一个不安全的块并继续前进。

通过这些修订,我们现在有以下内容:

static mut STASH: &i32 = &128;
fn f(p: &i32) { // still not good enoughunsafe {STASH = p;}
}

我们差不多完成了。要查看剩下的问题,我们需要写出Rust帮助我们省略的一些内容。这里写的f的签名实际上是以下的简写:

fn f<'a>(p: &'a i32) { ... }

这里,生命周期’a(发音为“tick A”)是f的生命周期参数。您可以将<'a>读作“for any lifetime”a,所以当我们编写fn f <'a>(p:&'a i32)时,我们将定义一个函数,该函数在任何给定的情况下引用i32生命周期’a。因为我们必须允许’a成为任何生命周期,所以如果它是可能的最小生命周期,事情就更好了:一个只是把f调用。然后,此分配成为争用的焦点:

STASH = p;

由于STASH用于程序的整个执行,因此它所拥有的引用类型必须具有相同长度的生命周期; Rust称之为’静态生命周期。但是p的引用的生命周期是’a,它可以是任何东西,只要它包含对f的调用。所以,Rust拒绝我们的代码:

error[E0312]: lifetime of reference outlives lifetime of borrowed content...--> references_static.rs:6:17|
6 | STASH = p;| ^|= note: ...the reference is valid for the static lifetime...
note: ...but the borrowed content is only valid for the anonymous lifetime #1defined on the function body at 4:0--> references_static.rs:4:1|
4 | / fn f(p: &i32) { // still not good enough
5 | | unsafe {
6 | | STASH = p;
7 | | }
8 | | }| |_^

在这一点上,很明显我们的函数不能接受任何引用作为参数。但它应该能够接受具有’静态生命周期的引用:在STASH中存储这样的引用不能创建悬空指针。事实上,以下代码编译得很好:

static mut STASH: &i32 = &10;
fn f(p: &'static i32) {unsafe {STASH = p;}
}

由于WORTH_POINTING_AT是静态的,因此&WORTH_POINTING_AT的类型是’静态i32,可以安全地传递给f。然后退后一步,注意当我们修正正确性时f的签名发生了什么:原始的f(p:&i32)最终为f(p:&'静态i32)。换句话说,我们无法编写一个函数来隐藏全局变量中的引用而不在函数的签名中反映该意图。在Rust中,功能的签名总是暴露身体的行为。

相反,如果我们确实看到一个带有像g(p:&i32)这样的签名的函数(或者写出生命周期,g <'a>(p:&'a i32)),我们可以告诉它它没有存储它参数p在任何可以比呼叫更长的地方。没有必要研究g的定义;单独的签名告诉我们g可以和不能用它的论点做什么。当您尝试建立对函数调用的安全性时,这一事实最终非常有用。

将引用作为参数传递

现在我们已经展示了函数的签名与其正文的关系,让我们来看看它与函数调用者的关系。假设您有以下代码:

// This could be written more briefly: fn g(p: &i32),
// but let's write out the lifetimes for now.
fn g<'a>(p: &'a i32) { ... }
let x = 10;
g(&x);

仅从g的签名中,Rust知道它不会在可能比呼叫更长的任何地方保存p:任何封闭呼叫的生命周期都必须适用于’a。因此Rust选择&x的最小可能生命周期:对g的调用。这符合所有约束条件:它不会超过x,并将整个调用包含在g中。所以这段代码通过了集合。

请注意,虽然g需要一个生命周期参数’a,但在调用g时我们不需要提及它。在定义函数和类型时,您只需要担心生命周期参数;使用它们时,Rust会为您推断生命。如果我们尝试将&x传递给我们之前的函数f,并将其参数存储在静态中,该怎么办?

fn f(p: &'static i32) { ... }
let x = 10;
f(&x);

这无法编译:引用&x必须不会超过x,但是通过将它传递给f,我们将它限制为至少与’static一样长。这里没有办法满足每个人,因此Rust拒绝了代码。

返回引用

函数引用某些数据结构,然后将引用返回到该结构的某个部分是很常见的。例如,这是一个返回对切片的最小元素的引用的函数:

// v should have at least one element.
fn smallest(v: &[i32]) -> &i32 {let mut s = &v[0];for r in &v[1..] {if *r < *s { s = r; }}s
}

我们以通常的方式从该函数的签名中省略了生命期。当函数将单个引用作为参数并返回单个引用时,Rust假定这两个引用必须具有相同的生命周期。明确地写出来会给我们:

give us:
fn smallest<'a>(v: &'a [i32]) -> &'a i32 { ... }

假设我们像这样调用最小的:

let s;
{let parabola = [9, 4, 1, 0, 1, 4, 9];s = smallest(&parabola);
}
assert_eq!(*s, 0); // bad: points to element of dropped array

从最小的签名,我们可以看到它的参数和返回值必须具有相同的生命周期,‘a。在我们的调用中,&parabola不能超过parabola本身;然而,最小的返回值必须至少与s一样长。没有可能满足两个约束的生命周期’,所以Rust拒绝代码:

error: `parabola` does not live long enough--> references_lifetimes_propagated.rs:12:5|
11 | s = smallest(&parabola);| -------- borrow occurs here
12 | }| ^ `parabola` dropped here while still borrowed
13 | assert_eq!(*s, 0); // bad: points to element of dropped array
14 | }| - borrowed value needs to live until here

移动s使其寿命明确包含在抛物线中解决问题:

{let parabola = [9, 4, 1, 0, 1, 4, 9];let s = smallest(&parabola);assert_eq!(*s, 0); // fine: parabola still alive
}

函数签名的生命周期让Rust评估您传递给函数的引用与函数返回的引用之间的关系,并确保它们被安全地使用。

包含引用的结构

Rust如何处理存储在数据结构中的引用?这是我们之前看到的同样错误的程序,除了我们将引用放在一个结构中:

// This does not compile.
struct S {r: &i32
}
let s;
{let x = 10;s = S { r: &x };
}
assert_eq!(*s.r, 10); // bad: reads from dropped `x`

Rust对引用的安全约束不能神奇地消失,因为我们在结构中隐藏了引用。不知何故,这些限制也必须最终适用于S.确实,鲁斯特持怀疑态度:

error[E0106]: missing lifetime specifier--> references_in_struct.rs:7:12|
7 | r: &i32| ^ expected lifetime parameter

每当引用类型出现在另一个类型的定义中时,您必须写出其生命周期。你可以这样写:

struct S {r: &'static i32
}

这表示r只能引用将持续程序生命周期的i32值,这是相当有限的。另一种方法是给类型一个生命周期参数
'a,并将其用于r:

struct S<'a> {r: &'a i32
}

现在,S类型具有生命周期,就像引用类型一样。您为S类创建的每个值都会获得一个新的生命周期’a’,这会受到您使用该值的限制。你在r中存储的任何引用的生命周期最好都包含’a’,并且’a’必须比你存储S的任何地方都要长。

回到前面的代码,表达式S {r:&x}创建一个带有生命周期’a的新S值。当你在r字段中存储&x时,你约束’a完全位于x的生命周期内。

赋值s = S {…}将此S存储在一个变量中,该变量的生命周期延伸到示例的末尾,约束’a比s的生命周期更长。现在,Rust已经达到了和以前一样的矛盾限制:'一定不能超过x,但必须至少和s一样长。没有令人满意的生命周期,Rust拒绝代码。灾难避免了!具有生命周期参数的类型在放置在其他类型中时如何表现?

struct T {s: S // not adequate
}

Rust持怀疑态度,正如我们尝试在S中放置引用而未指定其生命周期时一样:

error[E0106]: missing lifetime specifier--> references_in_nested_struct.rs:8:8|
8 | s: S // not adequate| ^ expected lifetime parameter

我们不能在这里留下S的生命周期参数:Rust需要知道T的生命周期与其S中的引用的生命周期如何相关,以便将相同的检查应用于它对S和普通引用的T。我们可以给出’静态寿命’。这有效:

struct T {s: S<'static>
}

使用此定义,s字段可能只借用为整个程序执行而存活的值。这有点限制,但它确实意味着T不可能借用局部变量; T的生命周期没有特殊限制。另一种方法是给T自己的生命周期参数,并将其传递给S:

he other approach would be to give T its own lifetime parameter, and pass that to S:
struct T<'a> {s: S<'a>
}

通过使用生命周期参数’a并在s的类型中使用它,我们允许Rust将T值的生命周期与其S所持有的参考值相关联。

我们之前展示了函数的签名如何公开它对我们传递它的引用的作用。现在我们已经展示了类似的类型:类型的生命周期参数总是揭示它是否包含有趣(即非静态)生命周期的引用,以及这些生命周期是什么。

例如,假设我们有一个解析函数,它接受一片字节,并返回一个包含解析结果的结构:

fn parse_record<'i>(input: &'i [u8]) -> Record<'i> { ... }

在没有查看Record类型的定义的情况下,我们可以告诉我们,如果我们从parse_record接收一个Record,那么它包含的任何引用必须指向我们传入的输入缓冲区,而不是其他地方(除了’静态值之外) 。事实上,内部行为的这种暴露是Rust要求包含引用以获取显式生命周期参数的类型的原因。没有理由Rust不能简单地为结构中的每个引用构成一个不同的生命周期,并且省去了编写它们的麻烦。 Rust的早期版本实际上就是这样做的,但开发人员发现它令人困惑:知道一个值何时从另一个值借用某些东西是有帮助的,特别是在处理错误时。

它不仅仅是具有生命周期的像S这样的引用和类型。 Rust中的每个类型都有生命周期,包括i32和String。大多数都是“静态的”,这意味着这些类型的价值可以在你喜欢的时间内存活;例如,Vec 是自包含的,在任何特定变量超出范围之前不需要删除。但像Vec <&'a i32>这样的类型的生命周期必须用’a:它必须在它的指示物仍处于活着状态时被丢弃。

不同的生命周期参数

假设您已经定义了包含两个引用的结构,如下所示:

struct S<'a> {x: &'a i32,y: &'a i32
}

两个引用都使用相同的生命周期’a。如果您的代码想要执行以下操作,这可能是一个问题:

let x = 10;
let r;
{
let y = 20;
{
let s = S { x: &x, y: &y };
r = s.x;
}
}

此代码不会创建任何悬空指针。对y的引用保留在s中,在y之前超出范围。对x的引用最终在r中,它不会比x更长。

然而,如果你试图编译它,Rust会抱怨你的生存时间不长,即使它显然也是如此。为什么Rust担心?如果仔细阅读代码,可以按照其推理:

•S的两个字段都是具有相同生命周期’a’的引用,因此Rust必须找到适用于s.x和s.y的单个生命周期。

•我们指定r = s.x,要求’a包含r的生命周期。

•我们用&y初始化s.y,要求’a不超过y的生命周期。

这些约束是不可能满足的:没有寿命比y的范围短,但比r更长。生锈。出现问题是因为S中的两个引用具有相同的生命周期’a。更改S的定义以使每个引用具有不同的生命周期可以修复所有内容:

he definition of S to let each reference have a distinct lifetime fixes everything:
struct S<'a, 'b> {x: &'a i32,y: &'b i32
}

根据这个定义,s.x和s.y具有独立的生命周期。我们用s.x做什么对我们在s.y中存储的内容没有影响,因此现在很容易满足约束:'a可以简单地是r的生命周期,‘b可以是s’。 (y的生命也适用于’b,但Rust试图选择有效的最小寿命。)一切都很好。功能签名可以具有类似的效果。假设我们有这样的函数:

fn f<'a>(r: &'a i32, s: &'a i32) -> &'a i32 { r } // perhaps too tight

这里,两个参考参数使用相同的生命周期’a,这可能会以与我们之前显示的方式相同的方式不必要地约束调用者。如果这是一个问题,您可以让参数的生命周期独立变化:

fn f<'a, 'b>(r: &'a i32, s: &'b i32) -> &'a i32 { r } // looser

这样做的缺点是,添加生命周期会使类型和功能签名更难以阅读。您的作者倾向于首先尝试最简单的定义,然后在代码编译之前放松限制。由于Rust不允许代码运行,除非它是安全的,只需等待被告知何时出现问题是一种完全可以接受的策略。

省略生命周期参数

到目前为止,我们已经在本书中展示了许多函数,这些函数返回引用或将它们作为参数,但我们通常不需要说明哪个寿命是哪个。生命周期是存在的; Rust只是让它们在它们应该是什么时相当明显时省略它们。

在最简单的情况下,如果您的函数没有返回任何引用(或其他需要生命周期参数的类型),那么您永远不需要为参数写出生命周期。 Rust只为每个需要一个点的地点分配不同的生命周期。例如:

struct S<'a, 'b> {x: &'a i32,y: &'b i32
}
fn sum_r_xy(r: &i32, s: S) -> i32 {r + s.x + s.y
}

此功能的签名是以下的简写:

fn sum_r_xy<'a, 'b, 'c>(r: &'a i32, s: S<'b, 'c>) -> i32

如果确实返回带有生命周期参数的引用或其他类型,Rust仍会尝试使明确的案例变得容易。如果在函数的参数中只出现一个生命周期,则Rust假定返回值中的任何生命周期必须是那个:

fn first_third(point: &[i32; 3]) -> (&i32, &i32) {(&point[0], &point[2])
}

写下所有生命周期,相当于:

fn first_third<'a>(point: &'a [i32; 3]) -> (&'a i32, &'a i32)

如果你的参数有多个生命周期,那么就没有理由选择一个而不是另一个作为返回值,而Rust会让你拼出正在发生的事情。

但作为一个最后的简写,如果你的函数是某种类型的方法并通过引用获取其自身参数,那么就打破了关系:Rust假定self的生命周期是给出返回值中的所有内容的生命周期。 (self参数指的是调用方法的值,Rust在C ++,Java或JavaScript中的等价物,或Python中的self。我们将在第198页的“使用impl定义方法”中介绍方法。)

例如,您可以编写以下内容:

struct StringTable {elements: Vec<String>,
}
impl StringTable {fn find_by_prefix(&self, prefix: &str) -> Option<&String> {for i in 0 .. self.elements.len() {if self.elements[i].starts_with(prefix) {return Some(&self.elements[i]);}}None}
}

find_by_prefix方法的签名是以下的简写:

fn find_by_prefix<'a, 'b>(&'a self, prefix: &'b str) -> Option<&'a String>

Rust认为无论你借什么借钱,你都是从自己借来的。同样,这些只是缩写,意味着有用而不会引入意外。当它们不是你想要的时候,你总能明确地写下生命。

共享与可变的

到目前为止,我们已经讨论过Rust如何确保任何引用都不会指向超出范围的变量。但还有其他方法可以引入悬空指针。这是一个简单的案例:

let v = vec![4, 8, 19, 27, 34, 10];
let r = &v;
let aside = v; // move vector to aside
r[0]; // bad: uses `v`, which is now uninitialized

旁边的赋值移动向量,使v未初始化,将r转换为悬空指针,如图5-7所示。

虽然v在r的整个生命周期内保持不变,但这里的问题是v的值被移动到其他地方,使v未初始化而r仍然引用它。当然,Rust捕获了错误:

error[E0505]: cannot move out of `v` because it is borrowed--> references_sharing_vs_mutation_1.rs:10:9|
9 | let r = &v;| - borrow of `v` occurs here
10 | let aside = v; // move vector to aside| ^^^^^ move out of `v` occurs here

在其整个生命周期中,共享引用使其引用只读:您可能不会分配给引用或将其值移动到其他位置。在此代码中,r的生命周期包含移动向量的尝试,因此Rust拒绝该程序。如果您按此处所示更改程序,则没有问题:

am as shown here, there’s no problem:
let v = vec![4, 8, 19, 27, 34, 10];
{let r = &v;r[0]; // ok: vector is still there
}
let aside = v;

在这个版本中,r早先超出范围,引用的生命周期在v被移到一边之前结束,一切都很好。

这是造成严重破坏的另一种方式。假设我们有一个方便的函数来扩展带有切片元素的向量:

fn extend(vec: &mut Vec<f64>, slice: &[f64]) {for elt in slice {vec.push(*elt);}
}

这是对矢量的标准库的extend_from_slice方法的一个不太灵活(并且非常不太优化)的版本。我们可以使用它从其他向量或数组的切片构建一个向量:

let mut wave = Vec::new();
let head = vec![0.0, 1.0];
let tail = [0.0, -1.0];
extend(&mut wave, &head); // extend wave with another vector
extend(&mut wave, &tail); // extend wave with an array
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0]);

所以我们在这里建立了一个正弦波周期。如果我们想要添加另一个波动,我们可以将矢量附加到自身吗?

extend(&mut wave, &wave);
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0,0.0, 1.0, 0.0, -1.0]);

这在随意检查中可能看起来很好。但请记住,当我们向向量添加元素时,如果其缓冲区已满,则必须分配具有更多空间的新缓冲区。假设wave以四个元素的空间开始,因此当extend尝试添加第五个元素时必须分配更大的缓冲区。内存最终如图5-8所示。

extend函数的vec参数借用wave(由调用者拥有),它为自己分配了一个带有8个元素空间的新缓冲区。但切片继续指向旧的四元素缓冲区,它已被删除。

这种问题并不是Rust独有的:在指向它们的同时修改集合是许多语言中的微妙领域。在C ++中,std :: vector规范提醒您“重新分配[向量缓冲区]”会使引用序列中元素的所有引用,指针和迭代器无效。“类似地,Java说,修改java.util。 Hashtable对象:

[I]在创建迭代器之后的任何时候,Hashtable都会在结构上被修改,除非通过迭代器自己的remove方法,迭代器将抛出ConcurrentModificationException。

这种错误特别困难的是它不会一直发生。在测试中,您的向量可能总是碰巧有足够的空间,缓冲区可能永远不会被重新分配,并且问题可能永远不会发生。但是,Rust报告了我们在编译时调用扩展的问题:

ust, however, reports the problem with our call to extend at compile time:
error[E0502]: cannot borrow `wave` as immutable because it is also borrowed as mutable--> references_sharing_vs_mutation_2.rs:9:24|
9 | extend(&mut wave, &wave);| ---- ^^^^- mutable borrow ends here| | || | immutable borrow occurs here| mutable borrow occurs here

换句话说,我们可以借用对向量的可变引用,并且我们可以借用对其元素的共享引用,但是这两个引用的生命周期可能不重叠。在我们的例子中,两个引用的生命周期都包含对extend的调用,因此Rust拒绝了代码。
这些错误都源于违反Rust的变异和共享规则:

•共享访问是只读访问。共享引用所借用的值是只读的。在共享引用的生命周期中,它的指示对象和从该指示对象可以到达的任何东西都不能被任何东西改变。在该结构中没有任何实时可变引用;它的拥有者是只读的;等等。它真的冻结了。

•可变访问是独占访问。可变引用借用的值只能通过该引用访问。在可变引用的生命周期中,没有其他可用的路径指向它的指示对象,或从那里可以到达的任何值。生命周期可能与可变引用重叠的唯一引用是您从可变引用本身借用的引用。

Rust报告扩展示例违反了第二条规则:因为我们借用了一个对wave的可变引用,所以可变引用必须是到达向量或其元素的唯一方法。对切​​片的共享引用本身是另一种到达元素的方法,违反了第二条规则。

但Rust也可以将我们的bug视为违反第一条规则:因为我们借用了wave元素的共享引用,元素和Vec本身都是只读的。您不能将可变引用借用为只读值。

每种引用都会影响我们可以对引用引用路径的值以及从引用对象到达的值(图5-9)。

请注意,在这两种情况下,导致指示对象的所有权路径无法在参考生命周期内更改。对于共享借用,路径是只读的;对于可变借款,它是完全无法访问的。所以程序没有办法做任何会使引用无效的事情。将这些原则简化为最简单的例子:

let mut x = 10;
let r1 = &x;
let r2 = &x; // ok: multiple shared borrows permitted
x += 10; // error: cannot assign to `x` because it is borrowed
let m = &mut x; // error: cannot borrow `x` as mutable because it is// also borrowed as immutable
let mut y = 20;
let m1 = &mut y;
let m2 = &mut y; // error: cannot borrow as mutable more than once
let z = y; // error: cannot use `y` because it was mutably borrowed

可以从共享引用重新借用共享引用:

let mut w = (107, 109);
let r = &w;
let r0 = &r.0; // ok: reborrowing shared as shared
let m1 = &mut r.1; // error: can’t reborrow shared as mutable

你可以从一个可变的参考资料中重新借用:

You can reborrow from a mutable reference:
let mut v = (136, 139);
let m = &mut v;
let m0 = &mut m.0; // ok: reborrowing mutable from mutable
*m0 = 137;
let r1 = &m.1; // ok: reborrowing shared from mutable,// and doesn't overlap with m0
v.1; // error: access through other paths still forbidden

这些限制非常严格。回到我们的尝试调用扩展(&mut wave,&wave),没有快速简便的方法来修复代码以按照我们的方式工作。 Rust在任何地方都应用这些规则:如果我们借用一个对HashMap中的键的共享引用,我们就不能借用对HashMap的可变引用,直到共享引用的生命周期结束。

但是有充分的理由:设计集合以支持不受限制的同时迭代和修改是困难的,并且通常会排除更简单,更有效的实现。 Java的Hashtable和C ++的向量并没有打扰,Python字典和JavaScript对象都没有精确定义这种访问的行为方式。 JavaScript中的其他集合类型可以,但结果需要更多的实现。 C ++的std :: map承诺插入新条目不会使指向地图中其他条目的指针无效,但通过做出这个承诺,该标准排除了更多高效缓存设计,如Rust的BTreeMap,它在树的每个节点中存储多个条目。

这是这些规则捕获的那种错误的另一个例子。考虑以下用于管理文件描述符的C ++代码。为了简单起见,我们只展示一个构造函数和一个复制赋值运算符,我们将省略错误处理:

 int descriptor;File(int d) : descriptor(d) { }File& operator=(const File &rhs) {close(descriptor);descriptor = dup(rhs.descriptor);}
};

赋值运算符很简单,但在这种情况下失败很严重:

File f(open("foo.txt", ...));
...
f = f;

如果我们为自己分配一个文件,rhs和*都是同一个对象,所以operator =关闭它要传递给dup的文件描述符。我们销毁了我们要复制的资源。

在Rust中,类似的代码是:

 Rust, the analogous code would be:
struct File {descriptor: i32
}
fn new_file(d: i32) -> File {File { descriptor: d }}
fn clone_from(this: &mut File, rhs: &File) {close(this.descriptor);this.descriptor = dup(rhs.descriptor);
}

(这不是惯用的Rust。有很好的方法可以为Rust类型提供他们自己的构造函数和方法,我们在第9章中对它们进行了描述,但前面的定义适用于这个例子。)

如果我们编写与使用File相对应的Rust代码,我们得到:

let mut f = new_file(open("foo.txt", ...));
...
clone_from(&mut f, &f);

当然,Rust拒绝编译这段代码:

error[E0502]: cannot borrow `f` as immutable because it is also
borrowed as mutable--> references_self_assignment.rs:18:25|
18 | clone_from(&mut f, &f);| - ^- mutable borrow ends here| | || | immutable borrow occurs here| mutable borrow occurs here

这应该看起来很熟悉。事实证明,两个经典的C ++错误 - 无法应对自我赋值,并使用无效的迭代器 - 是同样的潜在错误!在这两种情况下,代码都假定它正在修改一个值,同时咨询另一个值,而实际上它们都是相同的值。如果您不小心让调用memcpy或strcpy的调用源和目标在C或C ++中重叠,那么这就是bug可以采取的另一种形式。通过要求可变访问是独占的,Rust已经抵御了各种各样的日常错误。

共享和可变引用的不混溶性真正证明了它在编写并发代码时的价值。只有在某些值可变且在线程之间共享时才可能进行数据竞争 - 这正是Rust的参考规则所消除的。避免不安全代码的并发Rust程序不受构造的数据争用。当我们在第19章讨论并发性时,我们将更详细地介绍这一方面,但总的来说,并发性在Rust中比在大多数其他语言中使用起来要容易得多。

Rust的共享参考与C的const指针

在第一次检查时,Rust的共享引用似乎非常类似于C和C ++的const值指针。但是,Rust的共享引用规则要严格得多。例如,请考虑以下C代码:

int x = 42; // int variable, not const
const int *p = &x; // pointer to const int
assert(*p == 42);
x++; // change variable directly
assert(*p == 43); // “constant” referent's value has changed

p是const int *的事实意味着你不能通过p本身修改它的指示物:( * p)++是禁止的。但是你也可以直接将对象作为x,这不是const,并以这种方式改变它的值。 C系列的const关键字有其用途,但不是常量。

在Rust中,共享引用禁止对其引用的所有修改,直到其生命周期结束:

let mut x = 42; // nonconst i32 variable
let p = &x; // shared reference to i32
assert_eq!(*p, 42);
x += 1; // error: cannot assign to x because it is borrowed
assert_eq!(*p, 42); // if you take out the assignment, this is true

为确保值恒定,我们需要跟踪该值的所有可能路径,并确保它们不允许修改或根本不能使用。 C和C ++指针太不受限制,编译器无法检查这一点。 Rust的引用始终与特定的生命周期相关联,因此可以在编译时检查它们。|

采取武器对象海

自20世纪90年代自动内存管理的兴起以来,所有程序的默认架构都是对象之海,如图5所示。

如果你有垃圾收集并且你开始编写一个程序而没有设计任何东西会发生这种情况。我们都建立了这样的系统。

这种架构有很多优点,没有显示在图表中:初始进展很快,很容易入侵,而且几年之后,你可以毫无困难地证明完全重写。 (提示AC / DC的“高速公路到地狱”。)

当然,也有缺点。当一切都依赖于这样的其他一切时,很难单独测试,发展甚至考虑任何组件。

关于Rust的一个有趣的事情是,所有权模型在高速公路上加速了地狱。在Rust两个值中创建一个循环需要花费一些精力,使得每个值都包含指向另一个的引用。您必须使用智能指针类型(如Rc)和内部可变性 - 这是我们尚未涵盖的主题。 Rust更倾向于指针,所有权和数据流在一个方向上通过系统,如图5所示11。

我们现在提出这个问题的原因是,在阅读本章之后,想要直接运行并创建一个“结构之海”,所有这些都与Rc智能指针绑在一起,然后重新创建所有对象你熟悉的导向反模式。这对你不起作用。 Rust的所有权模式会给你带来一些麻烦。解决方法是做一些前期设计并建立一个更好的计划。
Rust就是将理解你的程序的痛苦从未来转移到现在。它的工作原理不合理:Rust不仅能让你理解为什么你的程序是线程安全的,甚至还需要一些高级的架构设计。

Programming Rust Fast, Safe Systems Development(译) 引用(第五章 完)相关推荐

  1. Programming Rust Fast, Safe Systems Development(译) 表达式(第六章 完)

    LISP programmers know the value of everything, but the cost of nothing. -Alan Perlis, epigram #55 在本 ...

  2. Programming Rust Fast, Safe Systems Development(译) 错误处理(第七章)

    I knew if I stayed around long enough, something like this would happen. -George Bernard Shaw on dyi ...

  3. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引...

    Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引 原文:Introduction to 3 ...

  4. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十五章:第一人称摄像机和动态索引

    代码工程地址: https://github.com/jiabaodan/Direct12BookReadingNotes 学习目标 回顾视景坐标系变换的数学算法: 熟悉第一人称摄像机的功能: 实现第 ...

  5. Programming Entity Framework-dbContext 学习笔记第五章

    ### Programming Entity Framework-dbContext 学习笔记 第五章 将图表添加到Context中的方式及容易出现的错误 方法 结果 警告 Add Root 图标中的 ...

  6. 《Credit Risk Scorecard》第五章: Development Database Creation

    第五章:Scorecard Development Process, Stage 3: Development Database Creation Selection of Characteristi ...

  7. JS高级程序设计读书笔记(第五章 引用变量)

    第五章 引用变量 Object 创建 Object 实例的方式有两种.第一种是使用 new 操作符后跟 Object 构造函数,另一种方式是使用对象字面量表示法. var person = new O ...

  8. Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十九章:法线贴图

    Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十九章:法线贴图 原文:Introduction to 3D Game P ...

  9. 第五章 线性规划方法 Linear Programming

    第五章 线性规划方法 Linear Programming 5.1 线性规划问题的一般形式 5.2 线性规划问题的解 5.2.1 基本解的产生与转换 5.2.2 基本可行解的产生与转换 5.2.3 基 ...

最新文章

  1. SPI、UART、I2C三种串行总线简介
  2. CentOS安装网络代理软件
  3. iOS - Swift NSSize 尺寸
  4. python语言支持苹果系统吗_Mac系统上的一款Python编程平台
  5. 遗传算法求解极大值问题
  6. SQL 性能优化梳理
  7. calico的两种网络模式BGP和IP-IP性能分析
  8. Spring Boot 动态注入的两种方式
  9. 《Python Cookbook 3rd》笔记(4.10):序列上索引值迭代
  10. docker开放的端口_docker-5-解决宿主机没有开放81端口却可以直接访问docker启动的81端口nginx容器的问题...
  11. 大数据Hadoop详细介绍(v2016)
  12. java webmldn,MLDN李兴华JAVA WEB视频教程(30集)_源雷技术空间
  13. 修改MAC地址的方法
  14. mariadb 的安装及基本配置
  15. DOS原理和常用命令详解示例
  16. c++priority_queue详解
  17. 在线学习及作业平台管理系统(ssm,mysql)
  18. linux下硬件检测工具,Linux硬件检测工具
  19. “故宫小书包”公益活动走进四川乡村学校
  20. redhat linux 查看内存大小,CentOS (RHEL) 系统管理中的查看内存插槽数、最大容量和频率...

热门文章

  1. Kotlin快速运用第四阶段(集合相关框架)
  2. Qemu core 调试Cannot access memory at address 0x7fbc6c792858
  3. 小瓦怕扫地机器人_小瓦扫地机器人青春版app下载-小瓦扫地机器人米家app下载v5.6.81 安卓版-西西软件下载...
  4. 塞拉利昂一公司计划投资10亿美元用于建设光伏农业项目
  5. 广州大喜事婚庆公司报价表
  6. BWAI学习记录003_使用Chaoslauncher和AI(Stardust)人机对战
  7. wacom板子在MACBOOK里用PS画画的时候,老是画着快捷键就都不能用
  8. 组态王中时间存access怎么存_组态王数据保存
  9. [置顶] 跳槽前夕的三年总结
  10. MySQL中的uuid函数是什么东西