前言

在上一篇《深入解析Rust所有权机制》一文中笔者介绍了 Rust 的所有权机制,本文将对 Rust 所有权的使用做进一步的补充,延伸出了一种新的概念——引用和借用。

在 Rust 中允许通过引用来实现在没有完全拥有某些数据的情况下,允许函数或变量使用这些数据。

先来对引用和借用做一个概念上的定义:

  • 引用(reference):是一种非拥有型指针。
  • 借用(borrow):创建对某个值的引用的操作称为借用。

在下文你将对这两个概念进行更深入的理解。

引用与借用

引用和解引用

与 C++ 不同,在 Rust 中,引用是通过 & 运算符显式创建,并通过 * 运算符显式解引用

1
2
3
let x = 10;
let r = &x; // &x 是对 x 的共享引用
assert!(*r == 10); // *r 是对 r 的显式解引用

共享引用和可变引用

  • 共享引用:允许您读取但不能修改其引用目标(默认)。

  • 可变引用:允许您读取和修改值。

1
2
3
4
5
6
7
8
9
let value1 = String::from("hello");
let mut value2 = String::from("rust");

let ref1 = &value1; // &value1 是对 value1 的共享引用
let ref2 = &mut value2; // &mut value2 是对 value2 的可变引用

println!("ref1->{}, ref2->{}", *ref1, *ref2); // ref1->hello, ref2->rust
(*ref1).push_str("###"); // `ref1` is a `&` reference, so the data it refers to cannot be borrowed as mutable
(*ref2).push_str("!!!"); // ref1->hello, ref2->rust!!!

在使用可变引用时需要注意:除了通过将引用操作由 &value2 修改为 &mut value2,还需要将原始声明值声明为可变,let value2 = String::from("rust"); 修改为 let mut value2 = String::from("rust");

引用的规则

  1. 在变量的生命周期内,只能存在一个或多个不可变引用,不能同时存在可变引用。
  2. 在变量的生命周期内,只能存在一个可变引用,不能同时存在多个可变引用。
1
2
3
4
5
6
7
8
fn main() {
let mut value = String::from("hello");

let ref1 = &value;
let ref2 = &mut value;

println!("{}, {}", ref1, ref2);
}

执行输出:

1
2
3
4
5
6
7
8
9
10
error[E0502]: cannot borrow `value` as mutable because it is also borrowed as immutable
--> src/main.rs:5:16
|
4 | let ref1 = &value;
| ------ immutable borrow occurs here
5 | let ref2 = &mut value;
| ^^^^^^^^^^ mutable borrow occurs here
6 |
7 | println!("{}, {}", ref1, ref2);
| ---- immutable borrow later used here
1
2
3
4
5
6
7
8
fn main() {
let mut value = String::from("hello");

let ref1 = &mut value;
let ref2 = &mut value;

println!("{}, {}", ref1, ref2);
}

执行输出:

1
2
3
4
5
6
7
8
9
10
error[E0499]: cannot borrow `value` as mutable more than once at a time
--> src/main.rs:5:16
|
4 | let ref1 = &mut value;
| ---------- first mutable borrow occurs here
5 | let ref2 = &mut value;
| ^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", ref1, ref2);
| ---- first borrow later used here

只要存在对一个值的共享引用,即使是它的拥有者也不能修改它,该值会被锁定。如果有某个值的可变引用,那么它就会独占对该值的访问权,在可变引用消失之前,即使是拥有者也无法使用该值。共享和修改保持完全分离对于内存安全至关重要,这使得 Rust 在编译期就避免数据竞争(Data Race)

生命周期

生命周期图解定义

参考阅读

数据竞争

数据竞争(Data Race):是并发操作中的一种常见错误,它发生在多个线程或进程同时访问共享数据,并且至少其中一个是写操作。

数据竞争可由以下行为造成:

  • 至少有两个并发的线程或进程同时访问同一块内存区域。
  • 至少有一个线程执行写操作。
  • 没有同步数据访问的机制。

数据竞争可能导致未定义的行为,在多线程或多进程环境中,由于并发操作的不确定性,可能会发生以下问题:

  • 读-写问题:一个线程正在读取共享数据的同时,另一个线程正在对其进行写操作,这可能导致读取到不一致和错误的数据。
  • 写-写问题:两个线程同时对相同的数据进行写操作,结果取决于执行的先后顺序,可能导致数据的丢失和错误的状态。

悬垂引用

悬垂引用(Dangling References):当程序中的引用指向已经被释放的内存区域时,就会出现悬垂引用问题。

悬垂引用会导致严重的运行时错误,包括崩溃、未定义的行为和安全性问题,这种情况通常发生在以下几种情形:

  1. 释放内存后继续使用:如果程序释放了某块内存,但之后仍然保留对该内存的引用,并尝试在后续代码中继续使用,就会导致悬垂引用。
1
2
3
int *ptr = malloc(sizeof(int));
free(ptr);
// 在这之后继续使用 ptr,这将是悬垂引用
  1. 在函数返回后引用局部变量:如果一个函数返回了指向局部变量的指针或引用,而调用者在函数返回后继续使用这个指针或引用,就会导致悬垂引用。
1
2
3
4
5
6
7
int *getLocalValue() {
int value = 42;
return &value;
}

int *ptr = getLocalValue();
// 在这之后使用 ptr,这将是悬垂引用

总结

本周因为要写 Rust 引用的相关内容,就把之前看到的微软的 Rust 培训教程也拿出来看了一下,发现微软的教程在基本内容这里写的是真好,十分地简单清晰明了。而且在阅读过程中我还发现了之前在 Rust 所有权理解有出错的地方,当然这个错误也写进了博客中😓。

看来知识的学习非左右参照理解不可了,甚至教材也只是偏重一方面,可能是易于理解深度不够,可能是深度足够难以理解,当然也有可能本身理解有出错或偏差争议的地方。很难说本博文对阅读者有多大的参考学习作用,最大了不起为笔者自己做个参考了。

计算机最大的权威是计算机本身,一切以自己的实践操作经验为标准,其它知识经验只能作为总结性的参考。