图书馆(库)无法弥补个人(程序员)能力的不足. --Mark Miller
- 引用定义: reference, 非拥有型指针(不影响值的生命周期), 受到borrow checker的检查.
- 引用约束: reference的生命周期不能超过所指向的值
- 借用定义: borrow, 创建引用的特定术语, 强调引用的约束.
区别 | Rust 引用 &T / &mut T | C++ 引用 T& | C 指针 T* |
---|---|---|---|
是否可为空? | 否, 不能是 null (受借用检查器保护) | 否, 不能是 nullptr | 是, 可以是 NULL (可能导致空指针解引用) |
是否需要解引用? | 否, 直接使用,不需要 * | 否, 直接使用,不需要 * | 是, 需要 * 进行解引用 |
是否有独立地址? | 否, 没有自己的地址 | 否, 没有自己的地址 | 是, 指针变量本身有地址 |
是否支持算术运算? | 否, 不支持 + 、- | 否, 不支持 + 、- | 是, 支持指针运算(如 ptr++ ) |
生命周期管理 | 是, 受 借用检查 保护,防止悬垂指针 | 否, 可能悬垂(手动管理生命周期) | 否, 容易发生悬垂指针 |
适用场景 | 安全的借用,确保数据有效 | 主要用于参数传递 | 指向动态内存、数组遍历 |
5.1 对值的引用
HashMap是标准库中的Hash表类型, 艺术家名称(String)映射其作品(Vec<string>), show函数用于打印Hash表中的内容.
核心问题: 根据move特性, 在HashMap没有实现Copy特型的情况下, 会将table的所有权move给show函数, table还原为未初始化, 且table在show函数结束后声明周期结束, 所有值drop. 如果后续还想继续使用table的话, 会失败.
解决方法: 使用reference, 它允许用户在不影响所有权的情况下访问值.
- 共享引用: 只读, 数量任意,
&e
(类型:&T
)产生共享引用, 实现了Copy特型 - 可变引用: 可写, 数量唯一,
&mut e
(类型:&mut T
)产生可变引用 - 共享引用和可变引用互斥存在
- 引用与拥有者的关系
- 共享引用存在时: 拥有者也不能修改
- 可变引用存在: 独占修改权和访问权, 拥有者也不能使用该值
5.1.a 艺术家程序 (所有权move)
use std::collections::HashMap;
type Table = HashMap<String, Vec<String>>;
fn show (table: Table) {
for (artist, works) in table { // for获取table所有权, 并drop消耗
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);
}
运行
works by Caravaggio:
The Musicians
The Calling of St. Matthew
works by Gesualdo:
many madrigals
Tenebrae Responsoria
works by Cellini:
Perseus with the head of Medusa
a salt cellar
5.1.b 艺术家程序 (共享引用borrow)
主要修改
// 函数定义: 参数使用引用
fn show (table: &Table) -> () { // 使用引用
for (artist, works) in table { // artist和works迭代的都是对应的共享引用
println!("Artist: {}", artist);
for work in works { // 迭代对元素的共享引用
println!(" {}", work);
}
}
}
// 函数调用: 使用共享指针
show(&table); // 传入引用
5.2 使用引用
引用的应用:
- 允许函数在不获取所有权的情况下访问或修改某个结构
5.2.1 显式/隐式解引用
特性 | 说明 |
---|---|
显式解引用 | 算数运算, 比较运算, 传参时传入具体值, 都需要显式解引用 |
隐式解引用 | 使用. 时无需使用* , 因为编译器可以通过. 确定这是一个引用 |
Rust显式解引用
let x = 10;
let r = &x; // &x是对x的共享引用
assert!(*r == 10); // 对r显式解引用
let mut y = 32;
let m = &mut y;
m +=
rust隐式解引用
struct Anime { name: &'static str, bechdel_pass: bool } // 'static表示静态生命周期
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.bechdel_pass, true);
assert_eq!((*anime_ref).name, "Aria: The Animation"); // 显式解引用
5.2.2 对引用变量赋值
引用变量可以重新赋值, 指向新的值.
fn main() {
let x = 10;
let y = 20;
let mut r = &x;
r = &y;
assert!(*r == 20);
}
5.2.3 对引用进行引用
- 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;
assert_eq!(rrr.y, 729); // 三重引用只需要一次解引用

5.2.4 比较引用
类似于.
自动多重解引用, Rust比较运算符也可以自动解引用追踪目标 (限制: 引用类型必须相同才能触发自动解引用)
如果比较的是引用本身(地址), 使用std::ptr::eq(rx, ry)
比较
fn main() {
let x = 10;
let y = 10;
let rx = &x;
let ry = &y;
let rrx = ℞
let rry = &ry;
assert!(rrx <= rry); // 比较运算符自动解引用
assert!(rrx == rry); // 比较运算符自动解引用
}
5.2.5 引用永不为空
Rust引用永远不为空 (防止悬空指针), 如果需要表示NULL, 使用Option<&T>来表示空指针, 因为该类型要求使用之前检查是否为None.
5.2.6 借用任意表达式结果值的引用
C++中只能对左值(拥有内存地址的变量)引用, Rust也可以对临时值引用
过程:
* Rust检测到需要为一个立即数创建引用
* Rust为立即数创建一个匿名变量, 其生命周期取决于:
* 如果立即将引用赋值给某个变量, 生命周期于引用相同
* 如果没有引用, 则生命周期在封闭语句块的末尾结束 (;
结束)
* Rust为匿名变量创建引用
fn factorial(n: usize) -> usize {
(1..n+1).product() // 计算所有成绩
}
fn main() {
let r = &factorial(6);
assert_eq!(r + &1009, 1729); // 算数运算符也可以自动解引用
}
5.2.7 对切片和特型对象的引用
需要携带额外信息的引用, 即胖指针, 常见的两个胖指针:
- 切片引用
- 特型对象引用
5.3 引用安全
5.3.1 引用的生命周期
#Rust-point
引用的生命周期(作用域)必须在值的生命周期(作用域)之内
fn main() {
let r;
{
let x = 1;
r = &x; // x生命周期结束
}
assert_eq!(*r, 1); // 错误: 引用值的生命周期已经结束
}
特性 | 说明 |
---|---|
生命周期定义 | Rust为每个值, 每个引用分配生命周期, 并在编译期间检查 (编译完成后引用只是一个普通地址, 但是在编译阶段通过检查并确保了其安全性) |
生命周期范围 | 起点: 被初始化的点 终点: Rust编译器断定不再使用的点 |
三个生命周期 | 1.值生命周期 2.引用变量生命周期: r引用变量本身初始化 -> r最后一次使用对应值引用 (注意: 是最后一次使用, 不是r本身drop, 因为r可以重新赋值) 3.引用生命周期: 上述实例中x借用的引用 (区分引用变量生命周期) |
检查规则 | 1.引用生命周期 ∈ 值生命周期 (否则产生悬空指针) 2.引用变量生命周期 ∈ 引用生命周期 总结: 引用变量生命周期 ∈ 引用生命周期 ∈ 值生命周期 (特别注意引用变量生命周期的定义) 推论(必要条件): 引用变量生命周期 ∈ 值生命周期 |
5.3.2 引用作为函数参数
static mut STASH: &i32; // 静态变量
fn f(p: &i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
unsafe {
STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
}
}
// 错误一: 静态变量必须初始化
// 错误二: mut静态变量不是线程安全, 即使单线程也存在可重入问题 (所以static mut STASH只能在unsafe块中声明)
实际上rust省略了一部分的定义, 就是关于生命周期的定义.
'a
表示生命周期是任意的:
- 可以是很短的 (仅在f()调用期间),
- 可以是很长的 (在调用者函数, 比如main()中)
- 但不能自动提升为static, 除非显式要求.
- p中存储的引用生命周期长度交给rust编译器来判断, 但是会确保最长不会超过函数本身生命周期
static mut STASH: &i32; // 静态变量
fn f<'a>(p: &'a i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
unsafe {
STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
}
}
根据生命周期原则: 引用生命周期和值生命周期至少都应该包含引用变量生命周期, 而引用变量生命周期为static, 所以引用生命周期和值生命周期至少是staic
static mut STASH: &i32 = &10; // 静态变量
fn f(p: &'static i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
unsafe {
STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
}
}
// 问题一解决: 初始化
// 问题二解决: 显式声明形参生命周期为static
5.3.3 引用传参
以下是错误实例: x生命周期在main()中, 最终的STASH引用变量生命周期是static, 已经超过了值生命周期, 所以编译错误.
static mut STASH: &i32 = &10; // 静态变量
fn f(p: &'static i32) { // p: 形参 = 引用变量生命周期 ∈ 值生命周期
unsafe {
STASH = p; // STASH = 引用变量生命周期 ∈ 值生命周期 -> 静态生命周期
}
}
fn main() {
let x = 10;
f(&x);
}
5.3.4 返回引用
下述代码功能: 返回最小元素的引用
fn smallest(v: &[i32]) -> &i32 {
let mut s = &v[0];
for r in &v[1..] {
if *r < *s { s = r; }
}
s // 返回最小值引用
}
Rust的生命周期标注, 说明返回引用生命周期必须于参数v具有相同的生命周期
fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
...
}
fn main() {
let s;
{
let parabola = [9, 4, 1, 0, 1, 4, 9];
s = smallest(¶bola);
}
assert_eq!(*s, 0); // 错误: 返回引用的生命周期于¶bola相同, 不能超出{}
}
正确使用
fn smallest<'a>(v: &'a [i32]) -> &'a i32 {
let mut s = &v[0];
for r in &v[1..] {
if *r < *s { s = r; }
}
s // 返回最小值引用
}
fn main() {
let s;
let parabola = [9, 4, 1, 0, 1, 4, 9];
s = smallest(¶bola);
assert_eq!(*s, 0);
}
5.3.5 包含引用的结构体
结构体声明:
- 结构体包含声明需要: 必须给出生命周期参数
- 核心思想: 值, 引用, 引用变量的生命周期包含关系, 只不过引用变量隐藏在结构体内部.
struct S {
r: &i32
}
fn main() {
{
let x = 10;
s = S { r: &x };
} // 值生命周期
assert_eq!(*s.r, 10); // 引用变量生命周期超出值生命周期
}
给定生命周期参数, 决定'a
生命周期长度的因素是: 存储在r中的引用生命周期, Rust会将'a生命周期完全限制在引用所指向的值的生命周期内部, 确保合法性.
struct S<'a> {
r: &'a i32
}
将具有生命周期参数的类型放在其他结构中, 同样需要给定参数
// 更常用的写法
struct D<'a> {
s: S<'a>
}
// 或者
struct E {
s: S<'static>
}
5.3.6 生命周期参数
注意: 生命周期参数限定的是存储在引用变量中的引用生命周期长度.
- 生命周期分析:
- x的引用: 引用变量r和s.x均没有超过x值的生命周期
- y的引用: s.y引用变量生命周期同样没有超过y的生命周期
- 报错原因:
- 结构声明时x和y成员的生命周期参数相同
- s.x成员存储的引用是x引用, 中间借用给了r, 这就要求引用生命周期至少大于r的生命周期
- s.y成员存储的引用是y引用, 中间借用给了s.y, 且引用生命周期不能大于y的值生命周期
- 二者冲突导致编译错误
- 解决方法: 使用两个生命周期参数来表示
struct S<'a> {
x: &'a i32,
y: &'a i32
}
fn main() {
let x = 10;
let r;
{
let y = 20;
{
let s = S { x: &x, y: &y };
r = s.x;
}
}
}
5.3.7 省略生命周期参数
常见情况:
- 自动标注: 无需返回引用或者其他带有生命周期参数的类型
- 自动标注: 无歧义情况, 比如只有一个参数, 返回值会假设具有相同的生命周期
- 自动标注: 实现一个struct的方法时, 默认从
&self
借用ref, 除非显示标注 - 显式标注: 多个参数, Rust无法判断哪个生命周期作为返回值生命周期
5.4 共享与可变
悬空指针的产生原因:
- 引用指向超出生命周期的变量 (上节讨论重点)
- 引用指向的变量所有权move (本节讨论重点)
- Rust解决方法: 引用存在的生命周期中, 引用目标保持只读, 不能move
// 非法操作
fn main() {
let v = vec![4, 8, 19, 27, 34, 10];
let r = &v; // r指向v
let aside = v; // 错误: 引用生命周期内, Rust保持v只读
r[0]; // r所指向的v未初始化
}
// 合法操作
fn main() {
let v = vec![4, 8, 19, 27, 34, 10];
{
let r = &v;
r[0];
} // 引用生命周期结束
let aside = v; // v恢复修改权限, 可以move
}

(实例) 切片扩展
//功能: 添加切片元素到向量中
fn extend(vec: &mut Vec<f64>, slice: &[f64]) {
for elt in slice {
vec.push(*elt);
}
}
fn main() {
let mut wave = Vec::new();
let head = vec![0.0, 1.0];
let tail = [0.0, -1.0];
extend(&mut wave, &head);
extend(&mut wave, &tail);
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0]);
}
如果添加自身的元素, 假设capacity已满, 可能需要重新申请缓冲区:
- slice引用变量仍然指向已经释放的缓冲区
- vec指向新的缓冲区
- 虽然vec是正确的, 但是很明显不能再解引用已经释放的slice, 导致错误
Rust将该问题简化: 不能同时出现共享引用和可变引用
extend(&mut wave, &wave);
assert_eq!(wave, vec![0.0, 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0]);

基本引用规则
fn main() {
let mut x = 0;
let r1 = &x;
let r2 = &x;
x += 10; // 错误: 借出共享引用后, x只读
let m = &mut x; // 错误: 共享引用和可变引用互斥存在
println!("{}, {}, {}", r1, r2, m); // 最后一次使用, 即为引用变量生命周期
}
所有权树借用规则
fn main() {
let mut v = (136, 139);
let m = &mut v;
let m0 = &mut m.0; // 允许对所有权树的子结点继续借用可变引用
let r1 = & m.1; // 允许继续借用共享引用
v.1; // 不允许通过除了可变引用以外的其他路径访问
}