所有权
所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。理解所有权是学习 Rust 的关键。
什么是所有权?
Section titled “什么是所有权?”在开始之前,让我们先了解内存管理的背景:
- C/C++:手动管理内存(malloc/free),容易出错
- Java/Python/Go:垃圾回收(GC),有运行时开销
- Rust:所有权系统,编译时检查,零运行时开销
理解栈和堆有助于理解所有权。
栈(Stack)
Section titled “栈(Stack)”- 后进先出(LIFO)
- 分配和释放非常快
- 存储大小固定的数据
- 例如:整数、浮点数、布尔值、固定大小的数组
堆(Heap)
Section titled “堆(Heap)”- 存储大小可变的数据
- 分配和释放相对较慢
- 需要指针来访问
- 例如:
String、Vec
fn main() { let x = 5; // 存储在栈上 let s = String::from("hello"); // 数据存储在堆上,s 是指向堆的指针}Rust 的所有权系统基于三条规则:
- 每个值都有一个所有者(owner)
- 值在任一时刻只能有一个所有者
- 当所有者离开作用域,值被丢弃(drop)
fn main() { { let s = String::from("hello"); // s 进入作用域,成为 "hello" 的所有者 println!("{}", s); } // s 离开作用域,"hello" 被丢弃,内存被释放}移动(Move)
Section titled “移动(Move)”栈上数据:复制
Section titled “栈上数据:复制”对于栈上的简单类型,赋值是复制:
fn main() { let x = 5; let y = x; // 复制,x 和 y 都是 5
println!("x = {}, y = {}", x, y); // 正常工作}堆上数据:移动
Section titled “堆上数据:移动”对于堆上的数据,赋值是移动所有权:
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 的所有权移动到 s2
// println!("{}", s1); // 错误!s1 不再有效 println!("{}", s2); // 正常工作}为什么?看看内存中发生了什么:
s1 (移动后无效) s2┌──────────────┐ ┌──────────────┐│ ptr ────┼──× │ ptr ────┼──┐│ len: 5 │ │ len: 5 │ ││ capacity: 5 │ │ capacity: 5 │ │└──────────────┘ └──────────────┘ │ │ 堆 ▼ ┌─────────────────┐ │ h │ e │ l │ l │ o │ └─────────────────┘如果允许 s1 和 s2 同时有效,当它们离开作用域时会尝试释放同一块内存——这就是”双重释放”错误。Rust 通过移动语义避免了这个问题。
克隆(Clone)
Section titled “克隆(Clone)”如果确实需要深拷贝堆上的数据,使用 clone():
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); // 深拷贝
println!("s1 = {}, s2 = {}", s1, s2); // 都有效}clone() 会在堆上创建数据的完整副本,可能开销较大。
Copy trait
Section titled “Copy trait”实现了 Copy trait 的类型在赋值时会复制而不是移动:
fn main() { let x: i32 = 5; let y = x; // 复制,因为 i32 实现了 Copy
println!("x = {}, y = {}", x, y);}实现 Copy 的类型包括:
- 所有整数类型(
i32、u64等) - 布尔类型
bool - 浮点类型(
f32、f64) - 字符类型
char - 只包含
Copy类型的元组,如(i32, i32)
规则:如果一个类型实现了 Drop trait,就不能实现 Copy。
所有权与函数
Section titled “所有权与函数”将值传递给函数和赋值类似——会移动或复制:
fn main() { let s = String::from("hello"); takes_ownership(s); // s 的所有权移动到函数 // println!("{}", s); // 错误!s 不再有效
let x = 5; makes_copy(x); // x 被复制 println!("{}", x); // 正常工作}
fn takes_ownership(some_string: String) { println!("{}", some_string);} // some_string 离开作用域,内存被释放
fn makes_copy(some_integer: i32) { println!("{}", some_integer);}返回值与所有权
Section titled “返回值与所有权”函数可以通过返回值转移所有权:
fn main() { let s1 = gives_ownership(); // 获得所有权
let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); // s2 移动进去,s3 获得返回的所有权}
fn gives_ownership() -> String { let some_string = String::from("hello"); some_string // 返回并移动所有权给调用者}
fn takes_and_gives_back(a_string: String) -> String { a_string // 返回并移动所有权给调用者}每次都转移所有权太麻烦了。引用允许你使用值但不获取所有权:
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // 传递引用
println!("'{}' 的长度是 {}", s1, len); // s1 仍然有效}
fn calculate_length(s: &String) -> usize { // s 是 String 的引用 s.len()} // s 离开作用域,但它不拥有数据,所以什么都不会发生创建引用的行为称为借用(borrowing)。
s1 s(引用)┌──────────────┐ ┌──────────────┐│ ptr ────┼──┐ │ ptr ────┼──┐│ len: 5 │ │ └──────────────┘ ││ capacity: 5 │ │ │└──────────────┘ │ │ ▼ │堆 └──────────────────────────┘┌─────────────────┐│ h │ e │ l │ l │ o │└─────────────────┘引用默认不可变。使用 &mut 创建可变引用:
fn main() { let mut s = String::from("hello"); change(&mut s); println!("{}", s); // 输出: hello, world}
fn change(some_string: &mut String) { some_string.push_str(", world");}Rust 的借用规则确保内存安全:
规则 1:可变引用是独占的
Section titled “规则 1:可变引用是独占的”同一时间只能有一个可变引用:
fn main() { let mut s = String::from("hello");
let r1 = &mut s; // let r2 = &mut s; // 错误!不能同时有两个可变引用
println!("{}", r1);}这防止了数据竞争。
规则 2:可变和不可变引用不能共存
Section titled “规则 2:可变和不可变引用不能共存”fn main() { let mut s = String::from("hello");
let r1 = &s; // 没问题 let r2 = &s; // 没问题 // let r3 = &mut s; // 错误!已经有不可变引用了
println!("{}, {}", r1, r2);}非词法作用域生命周期(NLL)
Section titled “非词法作用域生命周期(NLL)”引用的作用域从声明处开始,到最后一次使用结束:
fn main() { let mut s = String::from("hello");
let r1 = &s; let r2 = &s; println!("{} and {}", r1, r2); // r1 和 r2 在这之后不再使用,作用域结束
let r3 = &mut s; // 现在可以创建可变引用了 println!("{}", r3);}Rust 编译器保证不会有悬垂引用(指向已释放内存的引用):
fn main() { // let reference_to_nothing = dangle(); // 编译错误}
fn dangle() -> &String { // 错误!返回悬垂引用 let s = String::from("hello"); &s // 返回 s 的引用} // s 在这里被释放,引用将指向无效内存!解决方法是返回所有权:
fn no_dangle() -> String { let s = String::from("hello"); s // 移动所有权给调用者}切片(Slice)
Section titled “切片(Slice)”切片是对集合中部分元素的引用:
fn main() { let s = String::from("hello world");
let hello = &s[0..5]; // "hello" let world = &s[6..11]; // "world"
// 简写 let hello = &s[..5]; // 从开头 let world = &s[6..]; // 到结尾 let whole = &s[..]; // 整个字符串
println!("{} {}", hello, world);}字符串切片的类型是 &str。
字符串字面量是切片
Section titled “字符串字面量是切片”let s = "Hello, world!"; // s 的类型是 &str字符串字面量是指向二进制文件中某处的切片,这也是为什么它们是不可变的。
fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; // [2, 3],类型是 &[i32]
assert_eq!(slice, &[2, 3]);}练习 1:修复移动错误
Section titled “练习 1:修复移动错误”以下代码无法编译,请修复它:
fn main() { let s1 = String::from("hello"); let s2 = s1; println!("{}", s1); // 错误!}提示:有两种修复方法,尝试两种都实现。
练习 2:不获取所有权
Section titled “练习 2:不获取所有权”编写一个函数 string_length,计算字符串长度但不获取所有权:
fn string_length(/* 参数 */) -> usize { // 你的代码}
fn main() { let s = String::from("hello"); let len = string_length(/* 参数 */); println!("'{}' 的长度是 {}", s, len); // s 仍然可用}练习 3:可变借用
Section titled “练习 3:可变借用”编写一个函数 append_world,在字符串末尾添加 ”, world”:
fn append_world(/* 参数 */) { // 你的代码}
fn main() { let mut s = String::from("hello"); append_world(/* 参数 */); println!("{}", s); // 输出: hello, world}练习 4:理解借用规则
Section titled “练习 4:理解借用规则”以下代码为什么无法编译?如何修复?
fn main() { let mut s = String::from("hello");
let r1 = &s; let r2 = &s; let r3 = &mut s;
println!("{}, {}, {}", r1, r2, r3);}理解了所有权后,下一章我们将学习与之密切相关的概念——生命周期。