要点
所有权回答谁负责释放,借用回答谁暂时访问。值是否 Copy 由类型能力决定,不由“看起来很小”决定。
目录
以“检查标题是否为空”为例,检查函数只需 &str;调用之后仍要把原标题交给下一步保存,所以没有必要要求输入 String 并消费它。若函数要把标题长期存进自己的结构体,拥有 String 才是另一种明确契约,不要机械改成借用让生命周期更复杂。
验收: 在检查调用之后再次打印原标题,编译应通过;再把保存函数改为接收 String,解释为什么后续使用原变量会产生移动错误。若选择 clone,写明需要两份独立数据的原因,并用较大输入观察分配成本,而不是为消除编译错误无条件克隆。
核心概念
概念一:所有权三规则
定义: 所有权(ownership)是 Rust 的编译期内存管理模型——不靠垃圾回收,也不靠手动 free,而是把”资源何时释放”变成编译器可以验证的规则。
三条规则: 1. Rust 中每个值都有一个所有者(owner)——通常是某个变量 2. 同一时刻,一个值只能有一个所有者 3. 所有者离开作用域时,值被自动释放(调用 drop)
与内存管理方案的对比:
| 方案 | 代表语言 | 释放时机 | 代价 |
|---|---|---|---|
| 手动管理与 RAII | C / C++ | C 常需手动释放;现代 C++ 常通过 RAII 析构清理 | 所有权与借用约束需另外保证 |
| 垃圾回收 GC | Go / JS / Java | 运行时定期回收 | 运行时开销与停顿 |
| 所有权 | Rust | 编译期检查使用规则,资源在运行时按 drop 语义释放 | 学习和建模成本 |
概念二:move 语义与 Copy
定义: 把值赋给新变量或传入函数时,默认发生所有权转移(move)——原变量从此失效。
关键特性: – 堆上资源(如 String、Vec)默认 move:转移的是”栈上指针 + 元数据”,堆数据不复制 – 栈上简单类型(整数、浮点、bool、char 等)实现了 Copy:赋值按位复制,原变量仍可用 – 需要独立 String 内容时可用 clone();但 Clone 不保证深拷贝,Rc/Arc 的 clone 共享原分配
使用场景: – 场景一:函数接管一个资源(如把文件句柄传给消费者)→ 传所有权 – 场景二:只是读一下 → 传借用,避免 move(见概念三)
概念三:借用与借用检查器
定义: 借用(borrowing)是”临时使用但不接管所有权”的方式:&T 为共享借用(只读),&mut T 为独占借用(可写)。
两条核心规则(借用检查器在编译期强制执行): 1. 任意时刻,可以有任意多个不可变借用 &T,或恰好一个可变借用 &mut T 2. 不可变借用与可变借用不可同时存活
为什么这么设计: 对同一位置进行未同步的冲突访问且至少一个是写操作,会形成数据竞争;单线程中借用规则也能防止迭代期间修改集合导致引用失效。Rust 把这一保证下沉到编译期——不存在的数据竞争比运行时才发现的竞争更安全。
概念四:NLL —— 借用到”最后一次使用”为止
定义: NLL(Non-Lexical Lifetimes,非词法生命周期)指借用并非存活到作用域结束,而是存活到最后一次使用为止。
关键特性: – 现代 Rust 的借用检查基于”使用点”,而不是花括号边界 – 效果:直觉上顺序正确的代码(用完再改)大部分都能直接编译 – 这让”借用检查很烦”的老印象过时——先写自然版本,报错再调整,多数情况下 NLL 已经放行
实践指南
步骤一:亲手触发一次借用检查错误
目标: 建立”编译器是老师不是敌人”的工作方式——读错误信息是学 Rust 的核心技能。
操作指南:
cargo new borrow-lab && cd borrow-lab
- 把下方示例一中被注释的
println!("{s1}")一行取消注释 - 运行
cargo check - 阅读报错:
error[E0382]: borrow of moved value: s1,并注意提示里给出了修复建议 - 恢复注释,改成
clone版本再cargo check,观察差异
验证方法: 能复述出 E0382 报错中”值在哪一行被 move、又在哪一行被使用”两部分信息
步骤二:把函数签名改成借用风格
目标: 体会”参数优先借用”的惯例。
操作指南: 1. 写一个 fn print_len(s: String),调用两次同一个变量——第二次编译失败 2. 把签名改成 fn print_len(s: &String),两次调用都通过 3. 进一步按惯例改为 fn print_len(s: &str)(字符串切片,兼容字面量与 String)
验证方法: 同一变量能连续传参两次且不 clone
代码示例
以下四段是可用
rustc --edition 2024复核的完整示例。请在自己的工具链中编译、运行,并记录实际版本和输出。
示例一:move 语义
fn main() {
let s1 = String::from("所有权");
// s1 的所有权 move 给 s2,此后 s1 失效
let s2 = s1;
// println!("{s1}"); // ❌ 编译错误:value borrowed here after move
println!("{s2}");
// 整数等实现了 Copy 的类型按位复制,原变量仍可用
let n1 = 42;
let n2 = n1;
println!("n1 = {n1}, n2 = {n2}");
// 显式 clone 得到深拷贝,两个值各自独立
let s3 = s2.clone();
println!("s2 = {s2}, s3 = {s3}");
}
关键点解析: – 对 String 而言,move 转移的是这个值的所有权,不会要求把其管理的字符串内容复制一份;这是语言层面可依赖的结论。具体栈/堆布局属于实现细节,不应用来推断所有类型的成本。 – Copy 类型与堆类型的行为差异是初学者最常踩的认知差 – 这里 String 的 clone 复制字符串内容;其他类型须查 Clone 实现,不能一概推断成本
示例二:借用与借用规则
// 计算字符串长度:以借用方式接收,不夺取所有权
fn length(s: &String) -> usize {
s.len()
}
// 修改需要可变借用
fn push_world(s: &mut String) {
s.push_str(", world");
}
fn main() {
let mut s = String::from("hello");
// 不可变借用可以同时存在多个
let r1 = &s;
let r2 = &s;
println!("{r1} {r2} 长度 = {}", length(&s));
// r1、r2 在最后一次使用后借用即结束(NLL),此后才能创建可变借用
let r3 = &mut s;
r3.push_str("!");
println!("{r3}");
push_world(&mut s);
println!("{s}");
}
关键点解析: – 读用 &,写用 &mut——修改权限被显式建模进函数签名 – 多个只读借用并存是安全的,编译器放行 – r1/r2 最后一次使用在 println! 处,NLL 判定其借用随后结束,所以下一行的 &mut s 合法
示例三:NLL 让自然代码直接编译
fn main() {
let mut scores = vec![90, 85, 77];
// 不可变借用:读取第一个元素
let first = &scores[0];
println!("第一项 = {first}");
// first 在上一行之后不再使用,借用随之结束(NLL)
// 因此这里创建可变借用是允许的
scores.push(100);
println!("现在共 {} 项", scores.len());
// 反例(取消注释将编译失败):
// let x = &scores; // ❌ 不可变借用还活着
// scores.push(100); // ❌ 与 push 的可变借用冲突
// println!("{x:?}");
}
关键点解析: – first 用完后借用即结束,push 的可变借用不再与之冲突——这是 NLL 的典型收益 – 反例中 x 在 push 之后还要使用,借用必须活到那时,编译器正确拒绝(E0502)
示例四:所有权与借用的函数边界
// 返回所有权:最简单、最不容易出错的方式
fn make_greeting(name: &str) -> String {
format!("你好,{name}!")
}
// 返回借用:生命周期与输入借用一致;此处由编译器根据唯一输入借用推断。
fn first_word(sentence: &str) -> &str {
sentence.split_whitespace().next().unwrap_or("")
}
fn main() {
let text = "rust 跨平台 一把梭";
let word = first_word(text);
println!("第一个词: {word}");
let greeting = make_greeting("rustacean");
println!("{greeting}");
}
关键点解析: – 参数用 &str 而不是 String:借用让调用方保留所有权,&str 还能同时接收字面量 – 返回新建值 → 返回所有权;返回输入的一部分 → 返回借用(生命周期与输入绑定,深入的标注规则见后续生命周期篇) – 返回新建数据时通常返回拥有的值;仍须满足类型与所有权约束,移动返回不等于一定复制底层数据
最佳实践
函数只需临时读取数据时接受借用,确实要保存、转交或消费数据时接受所有权;签名应反映责任,而不是一律避免 String 或 Vec。clone 的行为取决于类型,可能复制数据,也可能增加共享持有者。
借用冲突先画出谁拥有数据、借用持续到哪一步,再缩短借用或调整操作顺序。不要盲目 clone 或添加 unsafe 消除诊断。用调用后还要再次使用原数据的例子,验证签名是否符合需求。
常见问题
Q1: 为什么可变借用和不可变借用不能同时存在?
A: 数据竞争的经典形态是”一些读者在读取的同时写者在修改”。Rust 在编译期强制读写互斥,从根源上消灭这类竞争。想要”多处共享 + 内部可变”时,标准库提供了 RefCell、RwLock 等把检查搬到运行时的类型(后续智能指针篇详解)。
Q2: 报 value borrowed here after move(E0382)怎么改?
A: 三选一:① 确实需要副本 → clone();② 只是需要访问 → 改传引用 &s;③ 所有权设计错了 → 调整数据流向,让唯一所有者负责整段生命周期。先读错误信息里”move 发生行”与”使用行”两个位置再动手。
Q3: Rust 没有 GC,会不会内存泄漏?
A: 所有权防的是 use-after-free、悬垂指针、数据竞争这类内存不安全问题。内存泄漏(如引用计数成环)在 Rust 里属于安全行为,不会引发未定义错误;需要防泄漏时用 Rc::downgrade 产生的 Weak 等手段(后续智能指针篇)。
交叉引用
本模块
- 📄 01 环境搭建 — 本文所有示例可直接在其 cargo 项目中运行
- 📄 03 结构体、枚举与模式匹配 — 下一步:把所有权规则应用到自己定义的数据类型上
- 📖 所有权细则字典 — 字典象限的单一事实来源,覆盖本文未展开的边界情况
跨模块
- 📄 09 Node.js 后端 — 带 GC 运行时(V8)的后端路线对照,帮助体会”编译期内存管理”的差异面
总结
核心要点回顾
- 三规则: 值有唯一所有者;赋值/传参默认 move;作用域结束自动释放
- 借用二分: 多个
&T或一个&mut T,读写互斥在编译期强制 - NLL: 借用存活到最后一次使用,自然顺序的代码大多直接编译
- 修复套路: clone / 改借用 / 重构所有权流向,三选一
学习成果检查
- [ ] 能解释 String 赋值后原变量为何失效、整数为何不受影响
- [ ] 能独立读懂并修复一次 E0382 或 E0502
- [ ] 函数签名习惯性使用借用参数
- [ ] 理解”借用检查器是编译期数据竞争防护”这一设计动机
最后更新: 2026年9月
🎯 下一步: 进入 03 结构体、枚举与模式匹配,用所有权与借用的视角定义你自己的类型。