所有权(Ownership)是 Rust 最独特且强大的特性,它使 Rust 能够在没有垃圾回收(GC)的情况下保证内存安全。理解所有权是掌握 Rust 的关键。
一、所有权三原则
Rust 的所有权系统基于三个核心规则:
-
每个值都有一个所有者(变量)
-
同一时间只能有一个所有者
-
当所有者离开作用域,值将被丢弃(内存被释放)
二、变量作用域与内存管理
{
let s = "hello"; // s 进入作用域
// 可以使用 s
} // s 离开作用域,相关资源被释放
对于堆分配的数据(堆变量):
{
let s = String::from("hello"); // 在堆上分配内存
// 使用 s
} // 自动调用 drop 方法释放堆内存
尽量不要对栈变量使用drop(),否则将出现如下警告信息。
warning: calls to `std::mem::drop` with a value that implements `Copy` does nothing
三、变量与数据交互的方式
3.1 复制(Copy)
对于栈数据,也就是基本类型(如整数、布尔值、字符等),赋值操作会复制数据,原变量仍然可用。这些类型都实现了 Copy
trait。
fn main() {
let x = 5;
let y = x; // 复制 x 的值给 y,x 仍然可用
println!("x = {}, y = {}", x, y); // 正确,输出 "x = 5, y = 5"
}
常见的 Copy 类型(栈变量):
- 所有整数类型(i32、u64 等)
- 布尔类型(bool)
- 字符类型(char)
- 浮点类型(f32、f64)
- 元组(仅当所有元素都是 Copy 类型时)
3.2 移动(Move)
对于堆数据,也就是非 Copy 类型采用所有权转移的方式。
对于复杂类型(如 String
、Vec
等),赋值操作会转移所有权(称为 "移动"),原变量将不再可用。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权转移给 s2(移动)
// 错误!s1 已失去所有权,不能再用。编译器将抛出 error[E0382]: borrow of moved value: `s1`
// println!("{}", s1);
println!("{}", s2); // 正确,s2 是当前所有者
}
为什么这样设计?
防止双重释放(double free)错误。如果 s1 和 s2 都拥有数据所有权,离开作用域时会尝试释放同一块内存,导致内存不安全。
3.3 克隆(Clone)
如果希望对非 Copy 类型进行深拷贝(不仅复制栈上的指针,还复制堆上的数据),可以使用 clone
方法。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 手动克隆,复制堆上的数据
println!("s1 = {}, s2 = {}", s1, s2); // 正确,两者都可用
}
四、函数与所有权
函数参数传递和返回值也可以发生所有权转移。
4.1 函数参数的所有权转移
fn main() {
let s = String::from("hello");
take_ownership(s); // s 的所有权转移到函数内部
// 错误!s 已失去所有权,编译错误:error[E0382]: borrow of moved value: `s`
// println!("{}", s);
}
fn take_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,内存被释放,所有权消亡
4.2 函数返回值的所有权转移
fn main() {
let s1 = gives_ownership(); // 函数返回值的所有权转移给 s1
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 所有权转移到函数,再转移给 s3
}
fn gives_ownership() -> String {
let some_string = String::from("yours");
some_string // 返回值所有权转移给调用者
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回值所有权转移给调用者
}
五、借用(Borrowing)
如果我们既想使用值,又不想转移所有权,可以使用引用(reference),这一行为称为 "借用"(borrowing)。引用的符号是 &
。
5.1 不可变借用
可以创建多个不可变引用,但不能通过不可变引用修改数据。
fn main() {
let s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 可以创建多个不可变引用
println!("{} and {}", r1, r2); // 正确
// 错误!不能通过不可变引用修改数据
// r1.push_str(", world");
}
使用函数时的不可变借用:
fn main() {
let s = String::from("hello");
let len = calculate_length(&s); // 传递引用,不转移所有权
println!("The length of '{}' is {}.", s, len); // s 仍然可用
}
// 参数是字符串的引用,不获取所有权
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开作用域,但不释放内存(因为它只是引用)
5.2 可变借用
可变引用允许修改数据,但有严格限制:
- 同一时间只能有一个可变引用
- 可变引用和不可变引用不能同时存在
fn main() {
//引用的源所有者必须为'可变'
let mut s = String::from("hello");
let r1 = &mut s; // 可变引用
// 错误!同一时间只能有一个可变引用,编译器抛出error[E0499]: cannot borrow `s` as mutable more than once at a time
// let r2 = &mut s;
r1.push_str(", world"); // 正确,通过可变引用修改数据
println!("{}", r1);
}
可变引用与不可变引用的冲突:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不可变引用
let r2 = &s; // 另一个不可变引用
// 错误!不能同时存在可变引用和不可变引用,编译器抛出error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
// let r3 = &mut s;
println!("{} and {}", r1, r2); // 正确,使用不可变引用
}
引用作用域:引用的作用域从声明处开始,到最后一次使用处结束。这允许在不同作用域中交替使用可变和不可变引用:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
r1.push_str(", world");
} // r1 离开作用域,可变引用失效
let r2 = &s; // 现在可以创建不可变引用
println!("{}", r2);
}
5.3 悬垂引用(Dangling References)
悬垂引用是指引用指向的内存已被释放的情况。Rust 编译时会阻止这种情况:
// 错误示例:尝试返回悬垂引用
fn dangle() -> &String {
let s = String::from("hello");
&s // s 会在函数结束时被释放,返回的引用将指向无效内存
}
解决方法:返回值本身而不是引用(转移所有权):
fn no_dangle() -> String {
let s = String::from("hello");
s // 返回所有权,调用者将成为新所有者
}
六、生命周期(Lifetimes)
生命周期用于描述引用的有效范围,确保引用不会比它指向的数据存活更久。大多数情况下,Rust 可以自动推断生命周期,但在复杂场景下需要手动标注。
6.1 生命周期标注语法
生命周期参数以 '
开头,通常使用小写字母(如 'a
、'b
)表示:
fn main() {
let s1 = String::from("abc");
let s2 = String::from("abcde");
let res = longest(&s1, &s2);
println!("res={}",res);
}
// 标注参数和返回值的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
上述代码中,'a
表示 x、y 和返回值必须拥有相同的生命周期,返回值的生命周期不能超过任何一个参数的生命周期。确保在读取函数返回值之时,不要出现函数返回值所引用的参数变量被释放的情况。
6.2 结构体中的生命周期
struct ImportantExcerpt<'a> {
part: &'a str, // part 的生命周期与结构体实例相同
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
6.3 生命周期省略规则
Rust 有一套生命周期省略规则,以下这些情况下无需手动标注。
1. 输入生命周期规则
每个引用参数都获得一个独立的生命周期参数。当函数有多个引用类型的参数时,编译器会为每个参数自动分配不同的生命周期参数。
示例代码(省略标注):
// 函数有两个引用参数,未标注生命周期
fn print_both(a: &str, b: &str) {
println!("{} and {}", a, b);
}
编译器推断后的完整形式(显式标注):
// 编译器为每个引用参数分配独立的生命周期 'a 和 'b
fn print_both<'a, 'b>(a: &'a str, b: &'b str) {
println!("{} and {}", a, b);
}
规则应用说明:函数有两个引用参数 a
和 b
,编译器自动为它们分配了不同的生命周期 'a
和 'b
,彼此独立,互不影响。
2. 输出生命周期规则
如果只有一个输入生命周期参数,它会被赋予所有输出生命周期参数。当函数只有一个引用类型的输入参数时,该参数的生命周期会被自动应用到所有引用类型的输出参数上。
示例代码(省略标注):
// 只有一个引用参数,返回值是引用类型,未标注生命周期
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
编译器推断后的完整形式(显式标注):
// 输入参数的生命周期 'a 被赋予输出参数
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
规则应用说明:函数只有一个引用参数 s
(生命周期 'a
),因此返回值的引用类型自动使用 'a
作为生命周期,确保返回的引用不会超过 s
的存活时间。
3. 方法规则
如果是方法,&self
或 &mut self
的生命周期会被赋予所有输出生命周期参数。在结构体 / 枚举的方法中(impl
块内),&self
或 &mut self
的生命周期会被自动应用到所有引用类型的返回值上。
示例代码(省略标注):
struct Text<'a> {
content: &'a str, // 结构体持有一个字符串切片
}
impl<'a> Text<'a> {
// 方法接收 &self,返回引用类型,未标注生命周期
fn get_first_part(&self) -> &str {
self.content.split('.').next().unwrap_or("")
}
}
编译器推断后的完整形式(显式标注):
struct Text<'a> {
content: &'a str,
}
impl<'a> Text<'a> {
// &self 的生命周期 'a 被赋予输出参数
fn get_first_part(&'a self) -> &'a str {
self.content.split('.').next().unwrap_or("")
}
}
规则应用说明:方法 get_first_part
接收 &self
(其生命周期与结构体的 'a
一致),因此返回值的引用类型自动使用 'a
作为生命周期,确保返回的引用不会超过结构体实例的存活时间。
生命周期省略规则的核心是编译器自动推断生命周期,减少手动标注的冗余。上述规则覆盖了绝大多数常见场景,只有在编译器无法确定生命周期关系时(如多个输入参数且输出引用可能来自任意一个),才需要手动标注生命周期。
七、切片(Slices)
切片是一种特殊的引用,用于访问集合中连续的一部分元素,而不获取所有权。切片也是理解所有权和借用的绝佳例子。
7.1 字符串切片
字符串切片的类型是 &str
,表示字符串的一部分:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // 从索引 0 到 4(不包含 5)
let world = &s[6..11]; // 从索引 6 到 10
println!("{} {}", hello, world); // 输出 "hello world"
}
简化写法:
&s[..n]
等价于&s[0..n]
&s[n..]
等价于&s[n..s.len()]
&s[..]
等价于&s[0..s.len()]
7.2 其他切片
除了字符串,其他集合类型(如数组)也可以创建切片:
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 数组切片,类型是 &[i32]
assert_eq!(slice, &[2, 3]); // 正确
}
八、总结
Rust 的所有权系统通过以下机制保证内存安全:
- 所有权规则确保每个值有唯一的所有者,离开作用域自动释放
- 借用(引用)允许临时使用值而不转移所有权
- 可变 / 不可变借用规则防止数据竞争
- 生命周期确保引用始终有效
虽然所有权系统带来一定的学习和开发成本,但它带来的内存安全和性能优势是 Rust 独特的价值所在。