03 - 引用
-
所有型指针:
- 当所有者被清除时,引用的资源也会随之清楚。
- 包括
Box<T>
堆指针、String
、Vec
值内部的指针。
-
非所有型指针:引用(reference)
- 当所有者被清除时,对它所引用的资源的生命期没有影响。
- 引用的生命期不能超过其引用的资源的生命期。
- 引用的本质是内存地址。
- 通过引用,可以访问值,又不会影响其所有权。
-
借用:创建对某个值的引用。(将引用类型作为函数参数的行为叫做借用)
-
引用的分类:
-
共享引用(shared reference):属于
&T
类型- 可以读取引用的值,但不能修改这个值。
- 在编译时,执行多读(multiple readers)的检查规则。
- 只要存在对某个值的共享引用,即便该值的所有者也不能修改它。
-
可修改引用(mutable reference):属于
&mut T
类型- 可以读取和修改引用的值。
- 在编译时,执行单写(single writer)的检查规则。
- 在可修改引用的存续期间,连对应值的所有者都无法使用。
-
-
重要原则:
- 保持共享引用和可修改引用分开,时保障内存安全的基本前提。
- 迭代对向量的共享引用,按照定义会产生对其元素的共享引用。
-
对函数传递参数:
- 传值(by value):以转移所有权的方式给函数传参。
- 传引用(by reference):传给函数的是对值的引用。
3.1 - 引用作为值
3.1.1-Rust 引用与 C++ 引用
-
在 Rust 中,引用是通过
&
操作符显示创建,解引用也要显示地使用*
操作符;let x = 10; let r = &x; // &x是对x的共享引用 assert!(*r == 10); // 显示地对r解引用
-
要创建可修改引用,则使用
&mut
操作符;let mut y = 32; let m = &mut y; // &mut y是y的课修改引用 *m += 32; // 显示地对m解引用,以便设置y的值 assert!(*m == 64); // 读取y的新值,同样要显示解引用
-
.
操作符:-
会在必要时,对其左侧操作数进行隐式解引用。
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"); // 等价于上一行代码,只不过显示地解引用 assert_eq!((*anime_ref).name, "Aria: The Animation");
-
在调用需要时,还可以隐式解用对其左侧操作数的引用。
let mut v = vec![1973, 1968]; v.sort(); // 隐式借用了对v的可修改引用 (&mut v).sort(); // 等价,不推荐
-
-
-
对比 C++:
- C++ 会隐式地在引用和左值(即引用内存位置的表达式)之间转换,这些转换会出现在任何需要用到它们的地方。
- 而 Rust,则必须使用
&
和*
操作符来创建和追随引用;只有.
操作符例外,它会隐式地借用和解引用。
3.1.2 - 给引用赋值
-
给 Rust 引用赋值,会导致它指向新值:
let x = 10; let y = 20; let mut r = &x; // 引用r开始时指向x;如果b为true,它就会指向y if b { r = &y; } assert!(*r == 10 || *r == 20);
-
C++ 中,给引用赋值会将值存储在引用中。而且,没有办法将 C++ 引用指向初始值之外的其他值。
3.1.3 - 引用的引用
Rust 允许引用的引用:
struct Point {
x: i32,
y: i32
}
let point = Point {
x: 100,
y: 729
};
// 下面的引用类型可以忽略
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr; // 3层引用
assert_eq!(rrr.y, 729);
3.1.4 - 比较引用
-
比较操作符:
-
与
.
操作符类似,可以 “看穿” 任意多个引用。 -
左右两个操作符的类型必须相同。
-
常用于泛型函数中。
let x = 10; let y = 10; let rx = &x; let ry = &y; let rrx = ℞ let rry = &ry; assert!(rrx <= rry); // false assert!(rrx == rry); // true
-
-
std::ptr::eq
方法:-
比较引用的地址;
-
即,比较两个引用是不是指向同一块内存。
assert!(rx == ry); // true assert!(std::ptr::eq(rx, ry)); // false,两个值在不同的内存地址
-
3.1.5 - 引用永远不为空
- Rust 引用永远不为空:
- 没有
NULL
(C 语言)或nullptr
(C++ 语言); - 不能将整数转换为引用,因此也不能把 0 转换成引用。
- 没有
Option<&T>
引用,可以实现一个要么是引用,要么什么也不是的类型。用以代替 C++ 中的nullptr
。None
为空指针。Some(r)
表示为非零地址,r
是&T
类型的值。- 在使用
Option<&T>
之前,必须检查它是否为None
。
3.1.6 - 借用对任意表达式的引用
-
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 永远不会让代码中出现悬空指针:
- 如果有超出匿名变量生命期,且永远不会用到的引用存在,那么 Rust 会在编译时发现,并报告给程序员。
- 此时,只需要把匿名变量的值保存到一个命名变量中,并在给它一个适当的生命期即可。
3.1.7 - 对切片和特型对象的引用
- 胖指针(fat point):包含 “某个值的地址”,以及 “与使用该值相关的必要信息” 的,一个两个字长的值。
- Rust 有两种胖指针:
- 对切片的引用:包含切片地址,及其长度信息。
- 特型对象(trait object):即对实现某种特型的一个值的引用。包含一个值的地址和指向与该值匹配的特型实现的指针。
- 两种胖指针的对比:
- 都不拥有自己指向的值。
- 生命期都不能超出目标值。
- 可以是可修改或共享的。
3.2 - 引用安全
3.2.1 - 借用局部变量
-
不能借用对一个局部变量的引用,然后将其拿到该变量的作用域之外:
// 使用一对{}表示作用域 { let r; { let x = 1; r = &x; } assert_eq!(*r, 1); // 错误,读取了局部变量x的值。 }
-
每个引用类型附加一个生命期(lifetime),表示可以安全使用引用的一个范围。
- 生命期是 Rust 在编译时虚构的东西;
- 生命期没有运行时表示:在运行时,引用就是一个地址,其生命期取决于自身的类型。
-
重要原则:
- 变量的生命期必须包含或涵盖从它那里借来的引用的生命期。
- 引用的生命期必须包含或涵盖保存它变量的生命期。即,引用的生命期小于等于变量的生命期。
- 对于一个引用向量而言,(作为向量元素)的所有引用的生命期拥有这个向量的变量的生命期。
let v = vec![1, 2, 3]; let r = &v[1];
3.2.2 - 接收引用作为参数 —— 阐述函数签名与函数体的关系
-
static
静态变量等价于全局变量:在程序启动时创建,并一直存活到程序终止时的值。-
所有静态变量都必须初始化;
-
只能在
unsafe
块中操作可修改静态变量static mut STASH: &i32 = &128; fn f<'a>(p: &'a i32) { // 这个函数接收一个具有任意给定生命期‘a的i32类型的引用。 unsafe { STASH = p; } }
-
-
生命期参数(lifetime), 上述代码中:
- 生命期
'a
(读作 “撇 A”)是f
的生命期参数; <'a>
表示:“对于任意生命期'a
“。
- 生命期
-
'static
静态生命期:形如STASH
的生命期与程序的整个过程一样长,所以它保存的引用类型的生命期,也必须同样长。-
上述示例代码中的函数不能接受任意引用作为参数,但可以接受一个具有
'static
生命期的引用;fn f(p: &'static i32) {STASH = p;}
-
只有静态变量的引用才不会导致
STASH
成为悬空指针。
-
3.2.3 - 将引用作为参数传递 —— 阐述函数与调用者的关系
-
一般情况下,只需要在定义函数时考虑生命期参数,而在使用它们时 Rust 会自动推断生命期。
-
如下代码会编译失败,引用
&x
不能活得比x
还长,而把它传给f()
,会强迫它活的跟'static
一样长:fn f(p: &'static i32) { to_do!(); } let x = 10; f(&x); // 引用&x是对普通变量的引用,当传递到函数`f()`内时,变成了静态变量的引用,所以不允许
3.2.4 - 返回引用
-
取得某个数据结构的引用,然后返回对该结构某一部分的引用。
// 返回一个对切片中最小元素的引用 // v至少包含一个元素 fn smallest<'a>(v: &'a [i32]) -> &'a i32 { let mut s = &v[0]; for r in &v[1..] { if *r < *s { s = r; } } s // 返回值为s } fn main() { let parabola = [9, 4, 1, 0, 1, 4, 9]; let s = smallest(¶bola); assert_eq!(*s, 0); }
-
如果一个函数接收一个引用作为参数,并返回一个引用,那么这两个引用必须具有相同的生命期。
-
函数签名中的生命期,让 Rust 可以评估传递给函数的引用和函数返回的引用之间的关系,从而确保安全使用它们。
3.2.5 - 结构体包含引用
-
当引用类型出现在另一个类型的定义中时,必须写出生命期参数:
struct S<'a> { r: &'a i32 } // 保存在r中的任何引用的生命期最好包含'a,而'a也必须比保存S的任何值都长寿。
-
通过类型的生命期参数,可以知道该类型是否包含具有相应(非
‘static
)生命期的引用,以及那些引用的情况如何。
3.2.6 - 不同的生命期参数
放宽限制的缺点:过多的生命期参数会导致类型和函数签名复杂难度。
fn f<'a>(r: &'a i32, s: &'a i32) -> &'a i32 { // 严格型,两个参数生命期都是'a
r
}
fn f<'a, 'b>(r: &'a i32, s: &'b i32) -> &'a i32 { // 宽松型
r
}
3.2.7 - 省略生命期参数
- 如果函数不返回任何引用(或其他需要生命期参数的类型),那么永远不需要写出生命期参数。
- 如果函数参数中只出现了一个生命期,那么 Rust 会假定返回值中的生命期都是该生命期。
- 函数参数中有多个生命期,Rust 会要求程序员自主决定。
- Rust 会假定
self
的生命期就是返回值需要的生命期。(Rust 的self
相当于 C++、Java 或 JavaScript 中的this
,或者 Python 中的self
)
3.3 - 共享与修改
-
可以借用向量的可修改引用,也可以借用对其元素的共享引用,但这两个引用的生命期不能重叠。
let mut x = 10; let r1 = &x; let r2 = &x; // 可以,允许多次共享借用 x += 10; // 错误,不能给x赋值,因为它已经被借用了 let m = &mut x; // 错误,不能借用x的可修改引用,因为它已经被借出了不可修改引用 let mut y = 20; let m1 = &mut y; let m2 = &mut y; // 错误,不能借两次可修改引用 let z = y; // 错误,不能用y作为只读赋值,因为它已经借出了可修改引用。
-
从共享引用借用共享引用时可以的:
let mut w = (107, 109); let r = &w; let r0 = &r.0; // 可以,共享引用可以再借用为共享应用 let m1 = &mut r.1; // 错误,共享引用不能再借用为可修改引用。
-
从可修改引用再借用可修改引用也是可以的:
let mut v = (136, 139); let m = &mut v; let m0 = &mut m.0; // 可以,可修改引用可以再借用为可修改引用 *m0 = 137; let r1 = &m.1; // 可以,可修改引用可以再借用为共享引用,且不与(可修改引用)m0重叠 v.1; // 错误,禁止通过其他路径访问。
-
只有某个值在线程间既可修改又可共享时,数据争用才会发生。
3.4 - 引用与对象
- Rust 实现了指针、所有权和数据流单向地通过整个系统。
- Rust 很难用结构体实现完全的类的效果。
- Rust 强迫程序员理解为什么自己的程序是线程安全的,甚至还会要求程序员在更高层面上进行一些架构设计。
详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第五章
链接地址