Rust 入门系列(第 3 篇,共 5 篇) 前置阅读:第 2 篇 - 基础语法与核心概念 阅读时间:30 分钟
引言
所有权(Ownership) 是 Rust 最独特也是最重要的特性。它让 Rust 在无需垃圾回收器的情况下,保证内存安全。
官方文档说:
“Ownership is Rust’s most unique feature, and it enables Rust to make memory safety guarantees without needing a garbage collector.”
这一篇会详细讲解所有权、借用和生命周期,理解这些概念后,你会对 Rust 有全新的认识。
一、为什么需要所有权?
1.1 内存管理的三种方式
1. 手动管理(C/C++)
int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr); // 必须手动释放- ✅ 性能高,无运行时开销
- ❌ 容易出错:内存泄漏、悬垂指针、双重释放
2. 垃圾回收(Java/Python/JavaScript)
String s = new String("hello");
// GC 自动回收,不需要 free- ✅ 安全,开发者无需关心
- ❌ 性能开销:GC 暂停、内存占用高
3. 所有权系统(Rust)
let s = String::from("hello");
// 离开作用域时自动释放- ✅ 安全:编译时检查
- ✅ 高性能:无 GC,无运行时开销
1.2 常见内存问题
悬垂指针:
int* dangling() {
int x = 42;
return &x; // ❌ x 已释放,返回无效指针
}双重释放:
int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr); // ❌ 重复释放内存泄漏:
void leak() {
int* ptr = malloc(sizeof(int));
// ❌ 忘记 free
}Rust 的所有权系统在编译时防止这些问题。
二、所有权规则
根据 Rust Book,所有权有三条核心规则:
- 每个值都有一个所有者(owner)
- 同一时间只能有一个所有者
- 所有者离开作用域时,值被丢弃(drop)
2.1 作用域与所有权
{ // s 无效,未声明
let s = "hello"; // s 有效
// 可以使用 s
} // s 离开作用域,不再有效2.2 String 与堆内存
let s1 = String::from("hello"); // 堆分配
let s2 = s1; // ⚠️ 移动(move),s1 不再有效
// println!("{}", s1); // ❌ 编译错误:s1 已被移动
println!("{}", s2); // ✅ OK发生了什么?
-
s1拥有字符串 -
s2 = s1时,所有权转移给s2 -
s1不再有效,避免双重释放
内存示意图:
s1 (无效)
|
X (所有权已转移)
s2 -> [ptr, len, cap] -> 堆内存 ["hello"]三、移动、克隆、复制
3.1 移动(Move)
let s1 = String::from("hello");
let s2 = s1; // 移动
// s1 不再有效规则:默认情况下,赋值会移动所有权。
3.2 克隆(Clone)
如果需要深拷贝,使用 .clone():
let s1 = String::from("hello");
let s2 = s1.clone(); // 克隆堆数据
println!("s1 = {}, s2 = {}", s1, s2); // ✅ OK开销:clone() 会复制堆数据,有性能成本。
3.3 复制(Copy)
栈上的简单类型会自动复制:
let x = 5;
let y = x; // 复制(不是移动)
println!("x = {}, y = {}", x, y); // ✅ OK实现了 Copy trait 的类型:
- 所有整数类型:
i32,u64等 - 布尔类型:
bool - 浮点类型:
f32,f64 - 字符类型:
char - 元组(如果所有成员都实现 Copy):
(i32, i32)
不能 Copy 的类型:
-
String -
Vec<T> - 任何分配堆内存的类型
四、函数与所有权
4.1 传递参数
fn main() {
let s = String::from("hello");
takes_ownership(s); // s 移动到函数中
// println!("{}", s); // ❌ s 不再有效
let x = 5;
makes_copy(x); // x 被复制
println!("{}", x); // ✅ x 仍有效
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_string 离开作用域,被 drop
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integer 离开作用域,但无需 drop(栈上)4.2 返回值与所有权
fn gives_ownership() -> String {
let s = String::from("hello");
s // 返回 s,所有权移出函数
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 返回 a_string,所有权转移
}
fn main() {
let s1 = gives_ownership(); // s1 获得所有权
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); // s2 移入,s3 获得所有权
}五、引用与借用
5.1 不可变引用
使用 & 创建引用,不获取所有权:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // 借用 s1
println!("'{}' 的长度是 {}", s1, len); // ✅ s1 仍有效
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s 离开作用域,但不 drop(没有所有权)规则:
- 可以有多个不可变引用
- 引用不能修改值
5.2 可变引用
使用 &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");
}关键限制:
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s; // ❌ 同一时间只能有一个可变引用
println!("{}", r1);5.3 借用规则
官方文档总结的规则:
- 同一时间,要么只有一个可变引用,要么有任意数量的不可变引用
- 引用必须始终有效(不能悬垂)
为什么?
- 防止数据竞争(data race)
- 保证内存安全
示例:编译错误
let mut s = String::from("hello");
let r1 = &s; // OK
let r2 = &s; // OK
// let r3 = &mut s; // ❌ 错误:已有不可变引用
println!("{} and {}", r1, r2);作用域结束后可以创建新引用:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 和 r2 在这里不再使用
let r3 = &mut s; // ✅ OK
println!("{}", r3);六、悬垂引用
6.1 什么是悬垂引用?
// C 语言的悬垂指针
int* dangle() {
int x = 42;
return &x; // ❌ 返回局部变量的指针
}6.2 Rust 如何防止?
fn dangle() -> &String { // ❌ 编译错误
let s = String::from("hello");
&s
} // s 被 drop,返回的引用无效编译器报错:
error[E0106]: missing lifetime specifier正确做法:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 移动所有权
}七、生命周期
7.1 为什么需要生命周期?
生命周期确保引用始终有效。
{
let r; // -------+-- 'a
// |
{ // |
let x = 5; // -+--'b |
r = &x; // | |
} // -+ |
// |
// println!("{}", r); // ❌ x 已销毁
} // -------+7.2 函数中的生命周期
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}编译错误:
error[E0106]: missing lifetime specifier为什么? 编译器不知道返回的引用来自 x 还是 y,无法判断生命周期。
7.3 生命周期标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}含义:
-
'a是生命周期参数 - 返回值的生命周期至少与
x和y中较短的一样长
使用示例:
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("{}", result); // ✅ OK
}
// println!("{}", result); // ❌ string2 已销毁
}7.4 结构体中的生命周期
struct ImportantExcerpt<'a> {
part: &'a str, // 引用的生命周期
}
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,
};
}含义:ImportantExcerpt 实例的生命周期不能超过 part 引用的数据。
7.5 生命周期省略规则
Rust 编译器会自动推断生命周期,某些情况下可以省略:
规则 1:每个引用参数都有自己的生命周期
fn foo(x: &i32, y: &i32)
// 等价于
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)规则 2:只有一个输入生命周期时,赋给所有输出
fn foo(x: &i32) -> &i32
// 等价于
fn foo<'a>(x: &'a i32) -> &'a i32规则 3:方法中,如果有 &self 或 &mut self,其生命周期赋给所有输出
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}八、常见错误与解决方案
8.1 值被移动后使用
错误:
let s = String::from("hello");
let s2 = s;
println!("{}", s); // ❌ s 已移动解决方案:
// 方案 1:克隆
let s = String::from("hello");
let s2 = s.clone();
println!("{} {}", s, s2); // ✅ OK
// 方案 2:借用
let s = String::from("hello");
let s2 = &s;
println!("{} {}", s, s2); // ✅ OK8.2 同时有可变和不可变引用
错误:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ❌ 已有不可变引用
println!("{} {}", r1, r2);解决方案:分离作用域
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // r1 最后使用
let r2 = &mut s; // ✅ OK(r1 已结束)
println!("{}", r2);8.3 返回局部变量的引用
错误:
fn dangle() -> &String {
let s = String::from("hello");
&s // ❌ s 会被 drop
}解决方案:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // ✅ 移动所有权
}九、实战示例
9.1 字符串切片
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 my_string = String::from("hello world");
let word = first_word(&my_string);
println!("{}", word); // "hello"
}9.2 防止悬垂引用
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
// s.clear(); // ❌ 编译错误:已有不可变引用 word
println!("{}", word);
}十、总结
10.1 核心概念
-
所有权三原则:
- 每个值有一个所有者
- 同一时间只有一个所有者
- 所有者离开作用域时,值被丢弃
-
借用规则:
- 同一时间:一个可变引用 OR 多个不可变引用
- 引用必须始终有效
-
生命周期:
- 确保引用始终有效
- 编译器自动推断(大部分情况)
- 必要时需要显式标注
10.2 何时用什么?
| 场景 | 方法 | 示例 |
|---|---|---|
| 转移所有权 | 移动 | let s2 = s1; |
| 深拷贝 | 克隆 | let s2 = s1.clone(); |
| 只读访问 | 不可变引用 | &s |
| 修改值 | 可变引用 | &mut s |
| 返回值 | 移动所有权 | return s; |
10.3 学习建议
- 多写代码:编译器错误信息很友好,会告诉你怎么改
- 先理解规则:不要死记硬背,理解为什么这样设计
- 借用优先:能借用就不要移动所有权
- 相信编译器:编译通过 = 内存安全
十一、下一步
11.1 练习建议
- 实现一个函数,返回字符串中最长的单词
- 创建一个
struct,包含引用字段,练习生命周期标注 - 尝试故意写出会导致数据竞争的代码,看编译器如何阻止
11.2 推荐资源
系列预告
第 4 篇:《Rust 入门(四):实战 CLI 工具》
- 构建一个文件搜索工具
- 使用 clap 处理命令行参数
- 文件 I/O 和错误处理
第 5 篇:《Rust 入门(五):Rust + WebAssembly》
参考资料
- Understanding Ownership - The Rust Programming Language
- What is Ownership? - Rust Book
- Rust Lifetimes: A Complete Guide - Earthly Blog
- Rust Ownership Explained (2025) - Medium
- Understanding Rust Memory Management - InfoWorld
系列导航
- 上一篇:Rust 入门(二):基础语法速通
- 下一篇:Rust 入门(四):实战项目 - 构建命令行工具 🦀