【本文正在参加金石计划附加挑战赛------第三期命题】
前言
Rust 的所有权系统是其最独特且区分于其他语言的特性之一,对语言的其余部分具有深远的影响。这一机制使得 Rust 能够在没有垃圾回收器的情况下确保内存安全,因此,深入了解 Rust 中的所有权运作方式至关重要。
所有权概述
所有权(ownership)是 Rust 进行管理内存的规则。
- 在某些编程语言中,配备了垃圾回收机制,该机制能够在程序执行过程中定期识别并清理不再使用的内存资源(例如js的垃圾回收机制)
- 而在另外一些语言里,则需要程序员自行负责内存的分配与释放 (例如C语言的malloc和free)
Rust : 每个值都由一个唯一 的所有者管理。一旦所有者超出其作用域,该值即会被释放。通过这样的所有权机制,Rust 能够保证内存资源的安全性和高效利用。
Rust作用域示例:
rust
fn main() {
{
// s 在这里无效,它尚未声明
let s = "hello"; // 从此处起,s 是有效的
// 使用 s
}
// 此作用域已结束,s 不再有效
}
所有权规则
1. 转移所有权
示例:a和b是否进行了所有权转移?
rust
let a = 5;
let b = a;
这段代码并没有发生所有权的转移。
原因: 代码首先将数值 5 绑定至变量 a,随后将 a 的值复制给 b。最终,a 和 b 的值均为 5。由于整数属于 Rust 的基本数据类型,它们是具有固定大小的简单值,因此 a 和 b 的值是通过自动复制的方式进行赋值的,并且这些值都存储在栈中,无需在堆上分配内存。
整个过程中的赋值都是通过值拷贝的方式完成(发生在栈中),因此并不需要所有权转移。
示例:s1与s2是否发生所有权转移?
rust
let s1 = String::from("hello");
let s2 = s1;
// println!("{}", s1);
println!("{}", s2); // 正常打印 "hello"
发生了所有权转移;
String
类型是一个复杂类型,指向了一个堆上的空间,这里存储着它的真实数据。
如果此时打印s1会发生什么呢?由于 Rust 禁止你使用无效的引用,你会看到以下的错误:
text
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:28
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
在其他编程语言中,可能遇到过"浅拷贝"(shallow copy)和"深拷贝"(deep copy)的概念。当提到仅复制指针、长度和容量而没有复制实际的数据时,这听起来很像是浅拷贝的过程。然而,在 Rust 中,由于同时使原始变量(如 s1
)失效,这一过程被特称为"移动"(move),而非简单的浅拷贝。这意味着,当我们将 s1
的值赋给 s2
时,实际上是将 s1
的所有权转移给了 s2
,并且 s1
不再有效。
2.克隆
rust
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2); // 正常打印 "hello hello"
3.函数传值与返回
基本数据类型 传值入函数不会发生所有权转移,原因是栈上数据拷贝,不涉及堆数据
rust
fn main() {
let s = String::from("hello"); // s 进入作用域
takes_ownership(s); // s 的值移动到函数里 所以到这里不再有效
println!("在move进函数后继续使用s: {}",s); // 报错
let x = 5; // x 进入作用域
makes_copy(x); // x 应该移动函数里,
// 但 i32 是 Copy 的,所以在后面可继续使用 x
println!("在move进函数后继续使用x: {}",x); // 打印5
} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
// 所以不会有特殊操作
fn takes_ownership(some_string: String) { // some_string 进入作用域
println!("{}", some_string);
} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
fn makes_copy(some_integer: i32) { // some_integer 进入作用域
println!("{}", some_integer);
} // 这里,some_integer 移出作用域。不会有特殊操作
rust
fn main() {
let s1 = gives_ownership(); // gives_ownership 将返回值
// 移给 s1
let s2 = String::from("hello"); // s2 进入作用域
let s3 = takes_and_gives_back(s2); // s2 被移动到
// takes_and_gives_back 中,
// 它也将返回值移给 s3
} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
// 所以什么也不会发生。s1 移出作用域并被丢弃
fn gives_ownership() -> String { // gives_ownership 将返回值移动给
// 调用它的函数
let some_string = String::from("hello"); // some_string 进入作用域.
some_string // 返回 some_string 并移出给调用的函数
}
// takes_and_gives_back 将传入字符串并返回该值
fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
a_string // 返回 a_string 并移出给调用的函数
}
4.引用与借用
Rust 提供了一种机制,允许像其他编程语言那样使用变量的指针或引用,这就是所谓的"借用"(Borrowing)。
在 Rust 中,借用是指获取一个变量的引用。这就好比在现实生活中,当你需要使用某人拥有的物品时,可以向他们借来使用。使用完毕之后,你必须将物品归还给原主人。同样地,在 Rust 中,当你借用一个变量时,实际上是在创建该变量的一个临时引用。使用完毕后,这个引用会自动失效,从而确保原变量的所有权不会受到影响。
1.引用与解引用
rust
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, y); //不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。
}
变量 x
存储了一个 i32
类型的值 5。y
则是 x
的一个引用。我们可以断定 x
的值为 5。 若要对 y
的值进行断言,则需要使用 *y
来解除引用,以获取 y
所指向的实际值(即解引用)。完成解引用后,就能访问 y
指向的整数值,并能够将其与 5 进行比较。
简而言之,通过 *y
我们可以从 y
引用中提取出 x
的值,进而验证其是否等于 5。这样做的原因是 y
本身只是一个指向 x
的指针,而 *y
表达式告诉编译器我们想要访问的是 y
指向的具体值。
2.不可变引用
我们用 s1
的引用作为参数传递给 calculate_length
函数,而不是把 s1
的所有权转移给该函数:
rust
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len();
s.push_str(", world"); // 报错,正如变量默认不可变一样,引用指向的值默认也是不可变的
}
- 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
calculate_length
的参数s
类型从String
变为&String
这里的&
符号即是引用,允许你使用值,但是不能获取其所有权
3.可变引用
首先,声明 s
是可变类型,其次创建一个可变的引用 &mut s
和接受可变引用参数 some_string: &mut String
的函数
rust
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
- 可变引用同时只能存在一个
- 可变引用与不可变引用不能同时存在
4.借用规则总结
- 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
- 引用必须总是有效的
所有权应用场景总结
1. 防止数据竞争 (Data Race Prevention)
在并发编程中,多个线程可能访问相同的资源,容易产生数据竞争。Rust 的所有权规则结合借用检查,确保在编译时检测出不安全的并发访问。
示例:多线程并发访问
-
Arc
管理共享所有权,允许多线程安全共享数据。 -
Mutex
确保同一时间只有一个线程访问数据。 -
Rust 的所有权系统确保线程安全,避免数据竞争。
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 使用 Arc 和 Mutex 来管理共享状态
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
2. 自动释放资源 (RAII)
Rust 的所有权和作用域规则使得资源的分配和释放自动化。文件、网络连接等资源在超出作用域时会自动释放,无需手动管理。
示例:文件操作
-
当
file
超出作用域时,Rust 会调用drop
自动关闭文件。 -
这种自动释放避免了资源泄露。
rust
use std::fs::File;
use std::io::{self, Write};
fn main() -> io::Result<()> {
let file_path = "example.txt";
// 创建文件并写入内容
{
let mut file = File::create(file_path)?;
writeln!(file, "Hello, Rust ownership!")?;
// 文件自动关闭:所有权离开作用域时触发 drop
}
// 文件可以重新打开进行其他操作
let content = std::fs::read_to_string(file_path)?;
println!("File content: {}", content);
Ok(())
}
3.零开销抽象 (Zero-Cost Abstraction)
所有权和生命周期结合,能高效地管理复杂数据结构而不增加运行时开销。
示例:使用 Vector 管理数据
rust
fn main() {
let mut numbers = vec![1, 2, 3];
for number in &numbers {
println!("{}", number);
}
numbers.push(4); // 所有权仍然属于 numbers,允许修改
println!("{:?}", numbers);
}
4.高效跨函数传递数据
通过所有权转移或借用,可以高效传递和操作大数据。
rust
fn process_data(data: String) -> usize {
println!("Processing data: {}", data);
data.len() // 数据的所有权在函数内使用完毕后自动释放
}
fn main() {
let data = String::from("Hello, Rust!");
let length = process_data(data); // 所有权转移给 process_data
println!("Data length: {}", length);
// println!("{}", data); // 编译错误,data 的所有权已转移
}