【本文正在参加金石计划附加挑战赛——第三期命题】
序
Rust 语言的所有权机制是 Rust 在编译阶段实现内存安全和并发安全的核心特色之一。通过所有权、借用和生命周期检查,Rust 有效地防止了数据竞争、空指针、悬垂指针等常见的内存错误。
本篇文章通过介绍所有权机制向读者诠释如何做到编程语言天生的内存安全和并发安全。
内存安全
所有权(Ownership)
在 Rust 中,每一个值都拥有一个“所有者”,并且一个值在同一时间只能有一个所有者。当所有者离开作用域时(即变量超出作用域),Rust 会自动释放这个值所占的内存。这种机制避免了悬垂指针和内存泄漏,因为每块内存都在明确的时刻被回收。
fn ownership_example() {
let s1 = String::from("Hello, Rust!"); // s1 拥有这个字符串
let s2 = s1; // 所有权转移到 s2,这里发生了 "移动"(Move)
// println!("{}", s1); // 编译错误:s1 的所有权已经被转移
println!("{}", s2); // s2 可以正常使用
}
fn main() {
ownership_example()
}
假如把第 5 行代码的注释放开的话,那么程序会报错,可以看一下错误描述:
从提示信息可以清楚地看到 s1 的所有权移动到了 s2 身上,Rust 规定同一时间一个值只有一个引用者,所以编译报错。
编译器提示使用 clone 方法避免所有权转移错误。
借用(Borrowing)
Rust 提供了“借用”概念,以允许多个地方访问同一块数据。借用分为“不可变借用”和“可变借用”:
- 不可变借用:可以有多个不可变借用,即允许多个地方同时只读访问数据。
- 可变借用:在同一时间只能有一个可变借用,防止数据竞争。
fn immutable_borrow_example() {
let s = String::from("Rust");
let len = calculate_length(&s); // 借用 s 的引用
println!("The length of '{}' is {}.", s, len); // s 仍然可以使用
}
fn calculate_length(s: &String) -> usize {
s.len() // 不可变借用,允许只读访问
}
calculate_length 函数借用了 s 的不可变引用。由于是不可变的,多处引用访问是安全的。
fn mutable_borrow_example() {
let mut s = String::from("Hello");
change_string(&mut s); // 可变借用
println!("{}", s); // s 被修改
}
fn change_string(s: &mut String) {
s.push_str(", world!"); // 修改可变引用的数据
}
可变借用同一时间只能有写一个,所有修改操作只有一个,避免了数据一致性问题。
生命周期(Lifetimes)
Rust 使用生命周期标注来帮助编译器跟踪引用的有效范围。通过生命周期标注,编译器可以确保引用在有效期间不会被释放,从而避免了悬垂引用。这对多线程并发尤其重要,因为它确保了跨线程共享数据的有效性。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn lifetime_example() {
let string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("The longest string is '{}'", result);
}
函数 longest 使用生命周期 'a 标注,确保返回的引用在输入引用的生命周期内有效,从而避免悬垂指针。
并发安全
use std::sync::Arc;
use std::thread;
fn concurrency_example() {
let data = Arc::new(vec![1, 2, 3, 4, 5]); // 使用原子引用计数(Arc)共享数据
let mut handles = vec![];
for i in 0..5 {
let data_clone = Arc::clone(&data); // 每个线程获得数据的一个引用
let handle = thread::spawn(move || {
println!("Thread {}: {:?}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap(); // 等待所有线程完成
}
}
Arc 用于实现线程间的共享所有权,确保数据在线程间安全共享,同时避免手动管理引用计数。这种模式下,Rust 的所有权和借用机制保证了线程安全。