11 KiB
Rust 所有权(Ownership)概念详解
所有权是 Rust 最独特的特性,它让 Rust 无需垃圾回收器就能保证内存安全。
目录
- 1. 什么是所有权
- 2. 所有权规则
- 3. 变量作用域
- 4. String 类型与内存分配
- 5. 移动(Move)
- 6. 克隆(Clone)
- 7. Copy 特征
- 8. 所有权与函数
- 9. 返回值与作用域
- 10. 引用与借用
- 11. 可变引用
- 12. 悬垂引用
- 13. 切片类型
- 14. 总结
1. 什么是所有权
所有权是一套用于管理内存的规则。在大部分编程语言中,内存管理通过以下方式处理:
- 手动管理(如 C/C++):程序员显式分配和释放内存
- 垃圾回收(如 Java/Go):运行时自动回收不再使用的内存
- 所有权系统(Rust):通过编译时检查的一套规则管理内存
为什么需要所有权?
- 无需垃圾回收,性能更高
- 无需手动管理内存,更安全
- 在编译期消除内存错误
2. 所有权规则
三条核心规则:
- Rust 中的每一个值都有一个所有者(owner)
- 值在任一时刻有且仅有一个所有者
- 当所有者离开作用域,值会被自动释放(调用
drop)
3. 变量作用域
fn main() {
// s 在这里无效,尚未声明
let s = "hello"; // s 从此处开始有效
// 可以使用 s
println!("{}", s);
} // 此作用域结束,s 不再有效
4. String 类型与内存分配
字符串字面量 "hello" 是不可变的,编译时已知,存储在二进制文件中。对于在运行时可变的字符串,使用 String 类型。
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() 追加字符串
println!("{}", s); // 输出: hello, world!
}
内存分配与释放
String 类型的数据存储在堆上:
- 栈:存储指针、长度、容量(固定大小,编译时已知)
- 堆:存储实际的字符串内容(编译时大小未知,运行时动态分配)
栈 堆
┌──────────┐ ┌──────────┐
│ ptr │────────>│ h e l l o│
│ len: 5 │ │ w o r l d│
│ cap: 5 │ └──────────┘
└──────────┘
当变量离开作用域时,Rust 自动调用 drop 函数,将堆内存归还给操作系统。
5. 移动(Move)
let x = 5;
let y = x; // x 的值被复制到 y(栈上数据,Copy 类型)
println!("{}", x); // ✅ 正常工作,x 仍可用
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权**移动**到了 s2
// println!("{}", s1); // ❌ 编译错误!s1 已失效
println!("{}", s2); // ✅ 正常工作
为什么需要移动?
如果 Rust 对堆数据也做浅拷贝,当 s1 和 s2 同时离开作用域时,它们都会尝试释放同一块堆内存,导致二次释放错误。
Rust 通过移动解决了这个问题:赋值后旧变量失效,只有一个所有者负责释放内存。
移动前: 移动后:
s1 → [堆内存] s2 → [堆内存]
s1 已失效 ❌
6. 克隆(Clone)
如果确实需要深度复制堆数据,使用 clone 方法:
let s1 = String::from("hello");
let s2 = s1.clone(); // 在堆上创建一份完整副本
println!("s1 = {}, s2 = {}", s1, s2); // ✅ 两者都可用
克隆后:
s1 → [堆内存1: "hello"]
s2 → [堆内存2: "hello"]
clone会执行深拷贝,包括堆数据- 代价较高,仅在确实需要时使用
7. Copy 特征
对于完全存储在栈上的数据类型,Rust 提供了 Copy 特征:
let x = 5;
let y = x; // x 被复制,而非移动
println!("{}", x); // ✅ 仍可使用
具有 Copy 的类型
- 所有整数类型:
u32、i64等 - 所有浮点类型:
f32、f64 - 布尔类型:
bool - 字符类型:
char - 元组(仅当所有元素都实现了 Copy):
(i32, bool)✅,(i32, String)❌
不具有 Copy 的类型
StringVec<T>- 任何实现了
Droptrait 的类型
8. 所有权与函数
将值传递给函数时,会发生移动或复制,规则与赋值相同:
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 的所有权移动到函数内
// println!("{}", s); // ❌ 编译错误!s 已失效
let x = 5;
makes_copy(x); // x 被复制到函数内(i32 是 Copy 类型)
println!("{}", x); // ✅ x 仍然可用
} // x 离开作用域,然后 s 离开作用域(但 s 已被移动,不会发生什么)
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,drop 被调用,内存释放
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,无特殊操作(栈数据)
9. 返回值与作用域
函数的返回值也可以转移所有权:
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);
} // s1、s3 离开作用域并被释放;s2 已被移动,不触发释放
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 返回 some_string,所有权移出
}
fn takes_and_gives_back(a_string: String) -> String {
a_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) // 返回字符串和它的长度
}
这种写法有些繁琐,Rust 提供了引用来简化。
10. 引用与借用
引用允许在不获取所有权的情况下使用值。
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 离开作用域,但由于它不拥有值的所有权,不会调用 drop
s1 → [堆内存: "hello"]
↑
&s1(引用,指向 s1 的值)
引用的规则
- 引用不获取所有权
- 创建引用的行为称为借用(borrowing)
- 借用者离开作用域时,不会释放底层数据
11. 可变引用
默认情况下,引用是不可变的。要修改借用的值,需要使用可变引用:
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!");
}
可变引用的限制
在任意给定时间,只能拥有以下两者之一:
- 一个可变引用
- 任意数量的不可变引用
let mut s = String::from("hello");
// ✅ 不可变引用可以同时存在多个
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// ✅ 不可变引用不再使用后,可以创建可变引用
let r3 = &mut s;
println!("{}", r3);
let mut s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 不可变借用
let r3 = &mut s; // ❌ 编译错误!不能同时存在不可变和可变引用
println!("{}, {}, {}", r1, r2, r3);
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ 编译错误!不能同时存在两个可变引用
println!("{}, {}", r1, r2);
这样设计的好处:在编译时防止数据竞争!
数据竞争发生的三个条件:
- 两个或多个指针同时访问同一数据
- 至少有一个指针用于写入
- 没有同步数据访问的机制
Rust 的规则直接在编译期消灭了数据竞争。
12. 悬垂引用
悬垂引用是指向已被释放内存的指针。Rust 编译器保证永远不会产生悬垂引用:
fn main() {
let reference_to_nothing = dangle(); // ❌ 编译错误!
}
fn dangle() -> &String {
let s = String::from("hello");
&s // 返回 s 的引用
} // s 离开作用域,内存被释放;但返回的引用仍指向已释放的内存
编译器错误信息:
error[E0106]: missing lifetime specifier
|
| fn dangle() -> &String {
| ^ expected named lifetime parameter
正确的做法是直接返回 String,将所有权移出去:
fn no_dangle() -> String {
let s = String::from("hello");
s // 所有权移出
}
13. 切片类型
切片是一种不获取所有权的引用,允许引用集合中一段连续的元素。
字符串切片
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
语法简写:
let s = String::from("hello world");
let hello = &s[..5]; // 从 0 开始
let world = &s[6..]; // 到末尾
let whole = &s[..]; // 整个字符串
字符串字面量就是切片
let s = "Hello, world!"; // s 的类型是 &str,即字符串切片
其他切片
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // 类型是 &[i32],包含 [2, 3]
assert_eq!(slice, &[2, 3]);
14. 总结
| 概念 | 说明 |
|---|---|
| 所有权 | 每个值有且仅有一个所有者;所有者离开作用域时值被释放 |
| 移动 | 赋值/传参时转移所有权,原变量失效 |
| 克隆 | 深拷贝堆数据,两个变量独立 |
| Copy | 栈上数据的自动复制(整数、布尔等基础类型) |
| 引用 | 不获取所有权的指针,允许多个存在 |
| 可变引用 | 可修改数据的引用,同时只能有一个 |
| 切片 | 对集合局部元素的引用 |
| 借用规则 | 任意时刻:要么一个可变引用,要么多个不可变引用 |
借用检查器保证了
- 内存安全:无悬垂指针、无二次释放、无使用后释放
- 线程安全:无数据竞争
- 零运行时开销:所有检查在编译时完成
所有权系统可能会让初学者感到"Rust 编译器在跟你作对",但请记住:编译器不是在刁难你,而是在保护你写出的每一行代码都是内存安全的。 当你熟悉所有权之后,这些规则会变得自然且合理。