Rust 所有权
所有权是 Rust 最独特的特性,它让 Rust 在没有垃圾回收器的情况下保证内存安全。理解所有权是掌握 Rust 的关键。
什么是所有权?
所有权是 Rust 用来管理内存的一套规则。与其他语言不同:
- C/C++:手动管理内存,容易出现内存泄漏和悬垂指针
- Java/Python:垃圾回收器自动管理,有运行时开销
- Rust:通过所有权系统在编译时管理内存,零运行时开销
所有权规则
Rust 的所有权遵循三条核心规则:
- Rust 中每个值都有一个所有者(owner)
- 同一时间只能有一个所有者
- 当所有者离开作用域,值会被丢弃
变量作用域
作用域是变量有效存在的范围:
fn main() {
// s 在这里无效,尚未声明
let s = "hello"; // 从此处起,s 开始有效
// 使用 s
println!("{}", s);
} // 作用域结束,s 不再有效
解释:
- 变量在声明时开始有效
- 变量在作用域结束时失效
- 当变量失效时,Rust 会自动清理其资源
String 类型
为了更好地理解所有权,我们需要区分栈上数据和堆上数据:
fn main() {
// 栈上的数据:固定大小,复制成本低
let x = 5;
let y = x; // 复制,x 和 y 都有效
println!("x = {}, y = {}", x, y);
// 堆上的数据:String 类型
let s1 = String::from("hello");
let s2 = s1; // 移动,s1 不再有效
// println!("{}", s1); // 错误!s1 已失效
println!("{}", s2); // 正确
}
String 与字符串字面量的区别:
fn main() {
// 字符串字面量:不可变,编译时已知大小,存储在二进制文件中
let s: &str = "hello";
// String:可变,运行时分配,存储在堆上
let mut s = String::from("hello");
s.push_str(", world!"); // 可以修改
println!("{}", s);
}
移动(Move)
当一个值被赋值给另一个变量时,所有权会被转移:
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
// s1 在这里已经无效
// println!("{}", s1); // 编译错误!
println!("{}", s2); // 正确
}
为什么需要移动?
String 由三部分组成:
- 指向堆上数据的指针
- 长度(len)
- 容量(capacity)
如果只是复制指针,会导致两个指针指向同一块堆内存,当它们离开作用域时,会尝试两次释放同一块内存(双重释放),这是严重的内存错误。
Rust 通过移动语义避免了这个问题:转移所有权后,原变量失效。
克隆(Clone)
如果需要深度复制堆上的数据,可以使用 clone 方法:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 深度复制
println!("s1 = {}, s2 = {}", s1, s2); // 两个都有效
}
解释:
clone会复制堆上的数据- 这是一个显式操作,代码中很容易看出
- 对于性能敏感的场景,需要谨慎使用
复制(Copy)
对于存储在栈上的简单类型,复制成本低,Rust 会自动复制:
fn main() {
let x = 5;
let y = x; // x 的值被复制到 y
println!("x = {}, y = {}", x, y); // 两个都有效
}
实现 Copy trait 的类型:
- 所有整数类型(i32, u64, etc.)
- 浮点类型(f32, f64)
- 布尔类型(bool)
- 字符类型(char)
- 元组(当所有元素都是 Copy 类型时)
- 不可变引用(&T)
fn main() {
// Copy 类型的例子
let a = 5;
let b = a; // 复制
println!("a = {}, b = {}", a, b);
let c = (1, 2.0, true); // (i32, f64, bool) 都是 Copy
let d = c;
println!("c = {:?}, d = {:?}", c, d);
// 非 Copy 类型的例子
let e = (1, String::from("hello")); // String 不是 Copy
let f = e; // 移动
// println!("{:?}", e); // 错误!
}
函数与所有权
传递值给函数
将值传递给函数也会发生所有权转移:
fn main() {
let s = String::from("hello");
take_ownership(s); // s 的所有权移动到函数
// println!("{}", s); // 错误!s 已失效
let x = 5;
make_copy(x); // x 是 Copy 类型,复制到函数
println!("{}", x); // 正确,x 仍有效
}
fn take_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,被丢弃
fn make_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,但因为是 Copy,不会有特殊操作
返回值与所有权
函数返回值也会转移所有权:
fn main() {
let s1 = gives_ownership(); // 函数将所有权移动给 s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 移动到函数,函数返回值移动给 s3
// println!("{}", s2); // 错误!s2 已失效
println!("s1 = {}, s3 = {}", s1, s3);
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 所有权移动给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 所有权移动给调用者
}
元组返回多个值
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("字符串 '{}' 的长度是 {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length) // 返回 String 和长度
}
引用和借用
传递所有权虽然安全,但有时不方便。Rust 提供了引用来解决这个问题。
引用
引用允许使用值但不获取所有权:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 传递引用
println!("字符串 '{}' 的长度是 {}", s1, len); // s1 仍然有效
}
fn calculate_length(s: &String) -> usize { // 参数是引用
s.len()
} // s 离开作用域,但因为没有所有权,不会丢弃数据
解释:
&String是对 String 的引用&s1创建一个指向 s1 的引用- 引用不拥有值,离开作用域时不会丢弃数据
- 这种行为称为"借用"(borrowing)
可变引用
默认引用是不可变的,如果需要修改,需要可变引用:
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 有两条引用规则,在编译时检查:
- 任意时刻,要么有一个可变引用,要么有任意数量的不可变引用
- 引用必须总是有效的
fn main() {
let mut s = String::from("hello");
// 规则 1:可变引用和不可变引用不能同时存在
let r1 = &s; // 不可变引用
let r2 = &s; // 不可变引用
// let r3 = &mut s; // 错误!已有不可变引用
println!("{} {}", r1, r2); // r1 和 r2 最后一次使用
let r3 = &mut s; // 正确!r1 和 r2 已不再使用
r3.push_str(" world");
println!("{}", r3);
}
非词法作用域生命周期(NLL):
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
// r1 和 r2 在这之后不再使用
let r3 = &mut s; // 正确!因为 r1 和 r2 已不再使用
println!("{}", r3);
}
悬垂引用
Rust 在编译时防止悬垂引用:
fn main() {
let reference_to_nothing = dangle();
println!("{}", reference_to_nothing);
}
// 错误示例
fn dangle() -> &String { // 返回 String 的引用
let s = String::from("hello");
&s // 返回 s 的引用
} // s 离开作用域被丢弃,返回的引用指向已释放的内存
// 正确做法
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回 String,所有权转移给调用者
}
切片
切片是对集合的部分引用,不拥有所有权。
字符串切片
fn main() {
let s = String::from("hello world");
// 字符串切片
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
// 简写
let hello = &s[..5]; // 从开头到索引 5
let world = &s[6..]; // 从索引 6 到结尾
let whole = &s[..]; // 整个字符串
println!("{} {}", hello, world);
}
字符串切片的类型是 &str:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("第一个单词: {}", word);
// 字符串字面量本身就是切片
let literal: &str = "hello world";
let word = first_word(&literal.to_string());
}
字符串切片作为参数
// 更好的函数签名,接受 &str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
// 可以接受 String 的切片
let s = String::from("hello world");
let word = first_word(&s);
println!("{}", word);
// 也可以接受字符串字面量
let literal = "hello world";
let word = first_word(literal);
println!("{}", word);
}
数组切片
fn main() {
let a = [1, 2, 3, 4, 5];
// 数组切片
let slice = &a[1..3]; // [2, 3]
println!("{:?}", slice);
}
所有权模式总结
fn main() {
// 1. 移动
let s1 = String::from("hello");
let s2 = s1; // s1 移动到 s2
// s1 不再有效
// 2. 克隆
let s3 = String::from("hello");
let s4 = s3.clone(); // 深度复制
// s3 和 s4 都有效
// 3. 复制(Copy 类型)
let x = 5;
let y = x; // 栈上复制
// x 和 y 都有效
// 4. 不可变引用
let s5 = String::from("hello");
let r1 = &s5; // 借用
// s5 仍有效,r1 可以读取
// 5. 可变引用
let mut s6 = String::from("hello");
let r2 = &mut s6; // 可变借用
r2.push_str(" world");
// s6 仍有效,但通过 r2 修改
// 6. 切片
let s7 = String::from("hello");
let slice = &s7[0..2]; // "he"
}
小结
本章我们学习了:
- 所有权规则:每个值有唯一所有者,离开作用域时释放
- 移动:赋值和函数传参会转移所有权
- 克隆:使用
clone深度复制堆数据 - 复制:Copy 类型会自动复制
- 引用和借用:
&T和&mut T - 引用规则:可变引用和不可变引用不能同时存在
- 切片:对集合的部分引用
&str、&[T]
练习
- 编写一个函数,接收 String 并返回第一个单词
- 解释为什么以下代码无法编译:
let s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{} {}", r1, r2); - 编写一个函数,交换两个可变引用指向的值
- 使用切片重构字符串处理函数