目标:深入理解 &T、&mut T、借用规则背后的设计哲学,掌握悬垂引用、NLL、切片 &str 与 &[T],并能用借用思维重构代码。
目录
- 一、从所有权的痛点说起
- 二、引用的本质
- 1. 引用是什么
- 2. 借用的心智模型
- 3. 自动解引用
- 4. 引用不是"弱化版指针"
- 三、不可变引用 &T
- 1. 可以同时有多个
- 2. 不能通过它修改
- 3. 函数参数中的不可变引用
- 4. 引用的作用域
- 四、可变引用 &mut T
- 1. 基本用法
- 2. 同一时刻只能有一个可变引用
- 3. 可变引用与不可变引用不能共存
- 4. NLL:非词法生命周期
- 五、借用规则的精确定义
- 六、悬垂引用
- 1. 什么是悬垂引用
- 2. 正确做法一:返回所有权
- 3. 正确做法二:返回传入引用的切片
- 4. 一个更隐蔽的例子
- 七、切片:引用的一段连续内存
- 1. 字符串切片 &str
- 2. 字符串字面量就是 &str
- 3. &str vs &String
- 4. 数组切片 &[T]
- 5. 切片是"胖指针"
- 八、借用与函数设计
- 1. 参数用借用还是所有权?
- 2. 返回借用还是新值?
- 3. 结构体持有引用需要生命周期标注
- 4. 实际项目中的借用策略
- 九、多元化练习题(含答案与解读)
- 十、知识点总结(第5课)
- 十一、下一课预告
一、从所有权的痛点说起
第4课我们知道,把 String 传给函数会发生移动:
fn main() {
let s = String::from("hello");
print_str(s);
println!("{}", s); // ❌ s 已被移动
}
fn print_str(s: String) {
println!("{}", s);
}
有两种笨办法。
办法一:函数把所有权还回来
fn print_str(s: String) -> String {
println!("{}", s);
s // 还回去
}
fn main() {
let s = String::from("hello");
let s = print_str(s); // 重新接收
println!("{}", s);
}
能用,但每个函数都要“借了还”,代码噪音大。
办法二:克隆一份
fn main() {
let s = String::from("hello");
print_str(s.clone()); // 复制堆数据
println!("{}", s);
}
能用,但每次调用都复制一份堆内存。如果字符串是几 MB,代价就大了。
引用就是来解决这个问题的。 它让你在不交出所有权的前提下访问数据。
下面是本节三种方案的对比流程图:
#mermaid-svg-dmWM68QsRWc9qvKB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dmWM68QsRWc9qvKB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dmWM68QsRWc9qvKB .error-icon{fill:#552222;}#mermaid-svg-dmWM68QsRWc9qvKB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dmWM68QsRWc9qvKB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dmWM68QsRWc9qvKB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dmWM68QsRWc9qvKB .marker.cross{stroke:#333333;}#mermaid-svg-dmWM68QsRWc9qvKB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dmWM68QsRWc9qvKB p{margin:0;}#mermaid-svg-dmWM68QsRWc9qvKB .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-dmWM68QsRWc9qvKB .cluster-label text{fill:#333;}#mermaid-svg-dmWM68QsRWc9qvKB .cluster-label span{color:#333;}#mermaid-svg-dmWM68QsRWc9qvKB .cluster-label span p{background-color:transparent;}#mermaid-svg-dmWM68QsRWc9qvKB .label text,#mermaid-svg-dmWM68QsRWc9qvKB span{fill:#333;color:#333;}#mermaid-svg-dmWM68QsRWc9qvKB .node rect,#mermaid-svg-dmWM68QsRWc9qvKB .node circle,#mermaid-svg-dmWM68QsRWc9qvKB .node ellipse,#mermaid-svg-dmWM68QsRWc9qvKB .node polygon,#mermaid-svg-dmWM68QsRWc9qvKB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dmWM68QsRWc9qvKB .rough-node .label text,#mermaid-svg-dmWM68QsRWc9qvKB .node .label text,#mermaid-svg-dmWM68QsRWc9qvKB .image-shape .label,#mermaid-svg-dmWM68QsRWc9qvKB .icon-shape .label{text-anchor:middle;}#mermaid-svg-dmWM68QsRWc9qvKB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dmWM68QsRWc9qvKB .rough-node .label,#mermaid-svg-dmWM68QsRWc9qvKB .node .label,#mermaid-svg-dmWM68QsRWc9qvKB .image-shape .label,#mermaid-svg-dmWM68QsRWc9qvKB .icon-shape .label{text-align:center;}#mermaid-svg-dmWM68QsRWc9qvKB .node.clickable{cursor:pointer;}#mermaid-svg-dmWM68QsRWc9qvKB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dmWM68QsRWc9qvKB .arrowheadPath{fill:#333333;}#mermaid-svg-dmWM68QsRWc9qvKB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dmWM68QsRWc9qvKB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dmWM68QsRWc9qvKB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmWM68QsRWc9qvKB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dmWM68QsRWc9qvKB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmWM68QsRWc9qvKB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dmWM68QsRWc9qvKB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dmWM68QsRWc9qvKB .cluster text{fill:#333;}#mermaid-svg-dmWM68QsRWc9qvKB .cluster span{color:#333;}#mermaid-svg-dmWM68QsRWc9qvKB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dmWM68QsRWc9qvKB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dmWM68QsRWc9qvKB rect.text{fill:none;stroke-width:0;}#mermaid-svg-dmWM68QsRWc9qvKB .icon-shape,#mermaid-svg-dmWM68QsRWc9qvKB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dmWM68QsRWc9qvKB .icon-shape p,#mermaid-svg-dmWM68QsRWc9qvKB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dmWM68QsRWc9qvKB .icon-shape .label rect,#mermaid-svg-dmWM68QsRWc9qvKB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dmWM68QsRWc9qvKB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dmWM68QsRWc9qvKB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dmWM68QsRWc9qvKB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
把 String 传给函数
如何处理?
方案一: 函数把所有权还回来
方案二: 克隆一份
方案三: 借用引用
每个函数都要借了还, 代码噪音大
每次调用都复制堆内存, 代价大
不交出所有权即可访问数据 ✅
二、引用的本质
1. 引用是什么
引用(reference)是一个指向某块数据的指针,但它不拥有那块数据。
fn main() {
let s = String::from("hello");
let r = &s; // r 是 &String,指向 s
println!("{}", r); // 可以读
println!("{}", s); // s 仍然有效
}
内存上,s 在栈上存着“指针 + 长度 + 容量”三要素,堆上存着 "hello"。r 是栈上另一个变量,存着指向 s 的地址。
栈 堆
┌──────────────┐
│ s │
│ ptr ────────┼──────► ┌─────────┐
│ len: 5 │ │ "hello" │
│ cap: 5 │ └─────────┘
└──────────────┘
┌──────────────┐
│ r │
│ ptr ────────┼──────► s
└──────────────┘
r 指向 s,s 指向堆。访问 r 时自动解引用。
2. 借用的心智模型
把所有权想象成一本书:
- let s = String::from("hello") —— 你买了一本书,你是主人
- print_str(s) —— 你把书送给别人,自己就没了
- print_str(&s) —— 你把书借给别人看,书还是你的
- print_str(&mut s) —— 你把书借给别人,允许他在上面做笔记
借书期间,书的所有权仍在你手里,但借阅规则限制了你能做什么。你不能在别人看书时把书撕了,也不能同时让两个人往上面写字。
3. 自动解引用
Rust 在方法调用和字段访问时会自动解引用:
let s = String::from("hello");
let r = &s;
println!("{}", r.len()); // 等价于 (*r).len()
r.len() 看起来像在 &String 上调方法,实际是自动解引用到 String 再调。这叫 Deref 强制转换。
4. 引用不是“弱化版指针”
很多语言里,引用就是指针的语法糖。Rust 里不一样:引用携带编译期的生命周期信息。编译器通过它追踪“这个引用能活多久、指向的数据是否还活着”。这是 Rust 引用和 C 指针的本质区别。
下面是引用与所有权关系的示意图:
#mermaid-svg-LROtmBGStWclAXgY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LROtmBGStWclAXgY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LROtmBGStWclAXgY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LROtmBGStWclAXgY .error-icon{fill:#552222;}#mermaid-svg-LROtmBGStWclAXgY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LROtmBGStWclAXgY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LROtmBGStWclAXgY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LROtmBGStWclAXgY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LROtmBGStWclAXgY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LROtmBGStWclAXgY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LROtmBGStWclAXgY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LROtmBGStWclAXgY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LROtmBGStWclAXgY .marker.cross{stroke:#333333;}#mermaid-svg-LROtmBGStWclAXgY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LROtmBGStWclAXgY p{margin:0;}#mermaid-svg-LROtmBGStWclAXgY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-LROtmBGStWclAXgY .cluster-label text{fill:#333;}#mermaid-svg-LROtmBGStWclAXgY .cluster-label span{color:#333;}#mermaid-svg-LROtmBGStWclAXgY .cluster-label span p{background-color:transparent;}#mermaid-svg-LROtmBGStWclAXgY .label text,#mermaid-svg-LROtmBGStWclAXgY span{fill:#333;color:#333;}#mermaid-svg-LROtmBGStWclAXgY .node rect,#mermaid-svg-LROtmBGStWclAXgY .node circle,#mermaid-svg-LROtmBGStWclAXgY .node ellipse,#mermaid-svg-LROtmBGStWclAXgY .node polygon,#mermaid-svg-LROtmBGStWclAXgY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LROtmBGStWclAXgY .rough-node .label text,#mermaid-svg-LROtmBGStWclAXgY .node .label text,#mermaid-svg-LROtmBGStWclAXgY .image-shape .label,#mermaid-svg-LROtmBGStWclAXgY .icon-shape .label{text-anchor:middle;}#mermaid-svg-LROtmBGStWclAXgY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LROtmBGStWclAXgY .rough-node .label,#mermaid-svg-LROtmBGStWclAXgY .node .label,#mermaid-svg-LROtmBGStWclAXgY .image-shape .label,#mermaid-svg-LROtmBGStWclAXgY .icon-shape .label{text-align:center;}#mermaid-svg-LROtmBGStWclAXgY .node.clickable{cursor:pointer;}#mermaid-svg-LROtmBGStWclAXgY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LROtmBGStWclAXgY .arrowheadPath{fill:#333333;}#mermaid-svg-LROtmBGStWclAXgY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LROtmBGStWclAXgY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LROtmBGStWclAXgY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LROtmBGStWclAXgY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LROtmBGStWclAXgY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LROtmBGStWclAXgY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LROtmBGStWclAXgY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LROtmBGStWclAXgY .cluster text{fill:#333;}#mermaid-svg-LROtmBGStWclAXgY .cluster span{color:#333;}#mermaid-svg-LROtmBGStWclAXgY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LROtmBGStWclAXgY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LROtmBGStWclAXgY rect.text{fill:none;stroke-width:0;}#mermaid-svg-LROtmBGStWclAXgY .icon-shape,#mermaid-svg-LROtmBGStWclAXgY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LROtmBGStWclAXgY .icon-shape p,#mermaid-svg-LROtmBGStWclAXgY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LROtmBGStWclAXgY .icon-shape .label rect,#mermaid-svg-LROtmBGStWclAXgY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LROtmBGStWclAXgY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LROtmBGStWclAXgY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LROtmBGStWclAXgY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
堆上
栈上
ptr 指向
ptr 指向
s: Stringptr | len | cap
r: &Stringptr
hello
三、不可变引用 &T
1. 可以同时有多个
不可变引用是只读的。多个人同时读同一本书没问题,所以可以共存:
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // ✅
}
2. 不能通过它修改
fn main() {
let s = String::from("hello");
let r = &s;
// r.push_str(" world"); // ❌ 不能通过不可变引用修改
}
3. 函数参数中的不可变引用
fn calculate_length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let len = calculate_length(&s);
println!("{} 的长度是 {}", s, len); // s 仍然可用
}
参数是 &String,调用时传 &s。函数拿到引用,不拿所有权,返回后 s 完好无损。
4. 引用的作用域
引用的生命周期从创建到最后一次使用为止(NLL 规则):
fn main() {
let s = String::from("hello");
let r = &s; // r 诞生
println!("{}", r); // r 最后一次使用
// 从这里开始 r 不再活跃
let s2 = String::from("world");
println!("{}", s2);
}
这个细节在讨论借用冲突时很关键,后面会反复用到。
四、可变引用 &mut T
1. 基本用法
想通过引用修改数据,用 &mut:
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
println!("{}", r); // hello world
println!("{}", s); // hello world
}
三处必须同时满足:
- 原变量声明为 mut
- 借用时用 &mut
- 接收方类型是 &mut T
2. 同一时刻只能有一个可变引用
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s; // ❌ 已有可变引用
println!("{} {}", r1, r2);
}
报错:
error[E0499]: cannot borrow `s` as mutable more than once at a time
为什么? 如果两个可变引用同时存在,同时写同一块内存,结果不可预测。Rust 从编译期杜绝这种数据竞争。
3. 可变引用与不可变引用不能共存
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ❌
println!("{} {}", r1, r2);
}
同理:有人正在读,就不能有人同时写。
4. NLL:非词法生命周期
关键点:Rust 判断借用冲突看的是实际使用范围,不是花括号范围。
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);
}
这段能编译,因为 r1、r2 在 println! 之后不再使用,生命周期结束。r3 创建时已经没有不可变引用活跃。
NLL(Non-Lexical Lifetimes)是 Rust 2018 引入的优化,让借用规则更符合直觉。它把“生命周期”从“作用域”中解放出来——引用活到它最后一次被使用,而不是活到花括号结束。
五、借用规则的精确定义
规则一:同一时刻,对同一数据,要么有任意多个不可变引用,要么有恰好一个可变引用。两者不能同时存在。
规则二:引用必须始终有效,不能悬垂。
用一句话概括:
共享不可变,独占可变。(Shared immutable, exclusive mutable.)
这条规则的三个“身份”:
- 读者:可以有多个,互不干扰
- 写者:只能有一个,独占访问
- 读写互斥:不能边读边写
这不是 Rust 发明的,而是并发编程中的经典原则——读写锁。Rust 把它从运行时提升到编译期强制。
为什么这条规则如此重要? 因为它一次性消灭了四类 bug:
- 数据竞争:两个线程同时写,或一读一写
- 迭代器失效:遍历时修改集合导致指针悬垂
- 悬垂引用:数据被释放后引用还在用
- 别名问题:编译器无法优化的根本原因
C++ 程序员花几十年总结的经验,Rust 用一条编译期规则全部覆盖。
借用规则可以用下面的流程图来概括:
#mermaid-svg-alv55Q7jvcexUU4g{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-alv55Q7jvcexUU4g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-alv55Q7jvcexUU4g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-alv55Q7jvcexUU4g .error-icon{fill:#552222;}#mermaid-svg-alv55Q7jvcexUU4g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-alv55Q7jvcexUU4g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-alv55Q7jvcexUU4g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-alv55Q7jvcexUU4g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-alv55Q7jvcexUU4g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-alv55Q7jvcexUU4g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-alv55Q7jvcexUU4g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-alv55Q7jvcexUU4g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-alv55Q7jvcexUU4g .marker.cross{stroke:#333333;}#mermaid-svg-alv55Q7jvcexUU4g svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-alv55Q7jvcexUU4g p{margin:0;}#mermaid-svg-alv55Q7jvcexUU4g .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-alv55Q7jvcexUU4g .cluster-label text{fill:#333;}#mermaid-svg-alv55Q7jvcexUU4g .cluster-label span{color:#333;}#mermaid-svg-alv55Q7jvcexUU4g .cluster-label span p{background-color:transparent;}#mermaid-svg-alv55Q7jvcexUU4g .label text,#mermaid-svg-alv55Q7jvcexUU4g span{fill:#333;color:#333;}#mermaid-svg-alv55Q7jvcexUU4g .node rect,#mermaid-svg-alv55Q7jvcexUU4g .node circle,#mermaid-svg-alv55Q7jvcexUU4g .node ellipse,#mermaid-svg-alv55Q7jvcexUU4g .node polygon,#mermaid-svg-alv55Q7jvcexUU4g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-alv55Q7jvcexUU4g .rough-node .label text,#mermaid-svg-alv55Q7jvcexUU4g .node .label text,#mermaid-svg-alv55Q7jvcexUU4g .image-shape .label,#mermaid-svg-alv55Q7jvcexUU4g .icon-shape .label{text-anchor:middle;}#mermaid-svg-alv55Q7jvcexUU4g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-alv55Q7jvcexUU4g .rough-node .label,#mermaid-svg-alv55Q7jvcexUU4g .node .label,#mermaid-svg-alv55Q7jvcexUU4g .image-shape .label,#mermaid-svg-alv55Q7jvcexUU4g .icon-shape .label{text-align:center;}#mermaid-svg-alv55Q7jvcexUU4g .node.clickable{cursor:pointer;}#mermaid-svg-alv55Q7jvcexUU4g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-alv55Q7jvcexUU4g .arrowheadPath{fill:#333333;}#mermaid-svg-alv55Q7jvcexUU4g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-alv55Q7jvcexUU4g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-alv55Q7jvcexUU4g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-alv55Q7jvcexUU4g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-alv55Q7jvcexUU4g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-alv55Q7jvcexUU4g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-alv55Q7jvcexUU4g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-alv55Q7jvcexUU4g .cluster text{fill:#333;}#mermaid-svg-alv55Q7jvcexUU4g .cluster span{color:#333;}#mermaid-svg-alv55Q7jvcexUU4g div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-alv55Q7jvcexUU4g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-alv55Q7jvcexUU4g rect.text{fill:none;stroke-width:0;}#mermaid-svg-alv55Q7jvcexUU4g .icon-shape,#mermaid-svg-alv55Q7jvcexUU4g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-alv55Q7jvcexUU4g .icon-shape p,#mermaid-svg-alv55Q7jvcexUU4g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-alv55Q7jvcexUU4g .icon-shape .label rect,#mermaid-svg-alv55Q7jvcexUU4g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-alv55Q7jvcexUU4g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-alv55Q7jvcexUU4g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-alv55Q7jvcexUU4g :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
多个不可变引用 &T
一个可变引用 &mut T
不可变 + 可变混合
多个可变引用
对同一数据
同时存在什么引用?
✅ 允许大家都能读
✅ 允许独占写
❌ 禁止读写冲突
❌ 禁止数据竞争
共享不可变, 独占可变
编译期报错
六、悬垂引用
1. 什么是悬垂引用
悬垂引用(dangling reference)是指引用指向的内存已经被释放:
fn dangle() -> &String { // ❌
let s = String::from("hello");
&s
} // s 离开作用域,堆内存被释放
编译报错:
error[E0106]: missing lifetime specifier
或者(如果加了生命周期标注):
error[E0515]: cannot return reference to local variable `s`
s 是函数内的局部变量,函数返回时被 drop,返回它的引用就成了悬垂指针。其他语言里这是运行时的未定义行为,Rust 在编译期就挡住。
2. 正确做法一:返回所有权
fn no_dangle() -> String {
let s = String::from("hello");
s // 移动所有权出去
}
3. 正确做法二:返回传入引用的切片
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' {
return &s[..i]; // 借用参数的一部分,安全
}
}
s
}
返回的引用来自参数,参数的寿命由调用方保证,安全。
4. 一个更隐蔽的例子
fn main() {
let r;
{
let s = String::from("hello");
r = &s; // ❌ s 在这里创建
} // s 在这里被 drop
println!("{}", r); // r 指向已释放内存
}
编译器报错 s does not live long enough。因为 r 在后面仍被使用,但 s 已经死了。这个例子说明:引用的寿命不能超过它指向的数据。
悬垂引用的产生与正确做法可以用下面的流程图说明:
#mermaid-svg-Fwnvu0wvOJPcnBgY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Fwnvu0wvOJPcnBgY .error-icon{fill:#552222;}#mermaid-svg-Fwnvu0wvOJPcnBgY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Fwnvu0wvOJPcnBgY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .marker.cross{stroke:#333333;}#mermaid-svg-Fwnvu0wvOJPcnBgY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Fwnvu0wvOJPcnBgY p{margin:0;}#mermaid-svg-Fwnvu0wvOJPcnBgY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster-label text{fill:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster-label span{color:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster-label span p{background-color:transparent;}#mermaid-svg-Fwnvu0wvOJPcnBgY .label text,#mermaid-svg-Fwnvu0wvOJPcnBgY span{fill:#333;color:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .node rect,#mermaid-svg-Fwnvu0wvOJPcnBgY .node circle,#mermaid-svg-Fwnvu0wvOJPcnBgY .node ellipse,#mermaid-svg-Fwnvu0wvOJPcnBgY .node polygon,#mermaid-svg-Fwnvu0wvOJPcnBgY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .rough-node .label text,#mermaid-svg-Fwnvu0wvOJPcnBgY .node .label text,#mermaid-svg-Fwnvu0wvOJPcnBgY .image-shape .label,#mermaid-svg-Fwnvu0wvOJPcnBgY .icon-shape .label{text-anchor:middle;}#mermaid-svg-Fwnvu0wvOJPcnBgY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .rough-node .label,#mermaid-svg-Fwnvu0wvOJPcnBgY .node .label,#mermaid-svg-Fwnvu0wvOJPcnBgY .image-shape .label,#mermaid-svg-Fwnvu0wvOJPcnBgY .icon-shape .label{text-align:center;}#mermaid-svg-Fwnvu0wvOJPcnBgY .node.clickable{cursor:pointer;}#mermaid-svg-Fwnvu0wvOJPcnBgY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .arrowheadPath{fill:#333333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fwnvu0wvOJPcnBgY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Fwnvu0wvOJPcnBgY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fwnvu0wvOJPcnBgY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster text{fill:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY .cluster span{color:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Fwnvu0wvOJPcnBgY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Fwnvu0wvOJPcnBgY rect.text{fill:none;stroke-width:0;}#mermaid-svg-Fwnvu0wvOJPcnBgY .icon-shape,#mermaid-svg-Fwnvu0wvOJPcnBgY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Fwnvu0wvOJPcnBgY .icon-shape p,#mermaid-svg-Fwnvu0wvOJPcnBgY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Fwnvu0wvOJPcnBgY .icon-shape .label rect,#mermaid-svg-Fwnvu0wvOJPcnBgY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Fwnvu0wvOJPcnBgY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Fwnvu0wvOJPcnBgY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Fwnvu0wvOJPcnBgY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
指向局部变量
指向参数
返回所有权
函数返回引用
引用指向哪里?
❌ 悬垂引用局部变量被 drop
✅ 安全参数由调用方保证存活
✅ 安全移动所有权出去
编译报错E0515 / E0106
七、切片:引用的一段连续内存
1. 字符串切片 &str
切片是对集合中一段连续元素的引用,不复制数据。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let all = &s[..]; // 整个字符串
let from6 = &s[6..]; // 从索引 6 到末尾
let to5 = &s[..5]; // 从开头到索引 5(不含)
}
语法是 &s[start..end],左闭右开。start 和 end 是字节索引,不是字符索引。
2. 字符串字面量就是 &str
let s: &str = "hello";
字符串字面量存在程序的二进制里(静态区),s 是指向它的引用。类型 &'static str,'static 表示和程序同寿命。
3. &str vs &String
&String:指向 String 的引用,必须来自一个 String 变量。
&str:字符串切片,可以来自 String、字符串字面量、另一个切片。
fn f(s: &str) { }
fn main() {
let s = String::from("hi");
f(&s); // ✅ &String 自动转 &str(Deref 强制转换)
f("hi"); // ✅ 字面量本身就是 &str
f(&s[0..1]); // ✅ 切片也是 &str
}
函数参数优先用 &str,因为它能接受更多类型。同理 &[T] 优于 &Vec<T>。
4. 数组切片 &[T]
fn main() {
let arr = [1, 2, 3, 4, 5];
let part = &arr[1..4]; // [2, 3, 4]
println!("{:?}", part);
let v = vec![10, 20, 30, 40];
let vpart = &v[2..]; // [30, 40]
println!("{:?}", vpart);
}
同一个函数可以同时处理数组和 Vec:
fn sum(slice: &[i32]) -> i32 {
slice.iter().sum()
}
fn main() {
let arr = [1, 2, 3];
let v = vec![4, 5, 6];
println!("{}", sum(&arr)); // 6
println!("{}", sum(&v)); // 15
}
&[i32] 是“最低要求”,数组和 Vec 都能自动转成它。这是 Rust API 设计的重要原则:参数用最宽松的类型,返回值用最具体的类型。
5. 切片是“胖指针”
切片不只是指针,还带长度。64 位机上占 16 字节:
&[T] = (指针: 8 字节, 长度: 8 字节)
所以切片访问时 Rust 能做边界检查,越界会 panic,不会读到垃圾内存。这是切片比裸指针安全的地方,代价只是多 8 字节存长度。
八、借用与函数设计
1. 参数用借用还是所有权?
经验法则:
- 只读,不修改 → &T(或 &str、&[T])
- 需要修改 → &mut T
- 需要存储或转移 → T(获取所有权)
- 需要消费并返回新值 → T 传入,新值返回
fn read_only(s: &str) { } // 只读
fn modify(s: &mut String) { } // 修改
fn store(s: String) { } // 拿走所有权
2. 返回借用还是新值?
返回借用:借用参数的一部分,调用方仍有所有权。
fn first_word(s: &str) -> &str { … }
返回新值:函数内部创建数据,所有权转给调用方。
fn make_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}
绝不能返回局部变量的引用:
fn bad() -> &String { // ❌
let s = String::from("hi");
&s
}
3. 结构体持有引用需要生命周期标注
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let s = String::from("hello world");
let ex = Excerpt { part: &s[..5] };
println!("{}", ex.part);
}
'a 表示 Excerpt 不能活得比它借用的 s 更长。这是生命周期在结构体上的应用,第10课会详细展开。
4. 实际项目中的借用策略
初学借用规则时,容易陷入“这里要不要 &”的纠结。一个实用的思路:
- 先写所有权版本(T),让它跑通
- 看编译器报错,如果提示“value moved”,就在调用处加 &
- 参数能借就借,返回值优先新值,除非明确要借用参数
借用规则不是一开始就要背下来的,编译器会一遍遍教你。写得多了,自然形成直觉。
函数参数与返回值的借用策略可以用下面的决策流程图来总结:
#mermaid-svg-ElGdknNEMG5AyVCl{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ElGdknNEMG5AyVCl .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ElGdknNEMG5AyVCl .error-icon{fill:#552222;}#mermaid-svg-ElGdknNEMG5AyVCl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ElGdknNEMG5AyVCl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ElGdknNEMG5AyVCl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ElGdknNEMG5AyVCl .marker.cross{stroke:#333333;}#mermaid-svg-ElGdknNEMG5AyVCl svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ElGdknNEMG5AyVCl p{margin:0;}#mermaid-svg-ElGdknNEMG5AyVCl .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ElGdknNEMG5AyVCl .cluster-label text{fill:#333;}#mermaid-svg-ElGdknNEMG5AyVCl .cluster-label span{color:#333;}#mermaid-svg-ElGdknNEMG5AyVCl .cluster-label span p{background-color:transparent;}#mermaid-svg-ElGdknNEMG5AyVCl .label text,#mermaid-svg-ElGdknNEMG5AyVCl span{fill:#333;color:#333;}#mermaid-svg-ElGdknNEMG5AyVCl .node rect,#mermaid-svg-ElGdknNEMG5AyVCl .node circle,#mermaid-svg-ElGdknNEMG5AyVCl .node ellipse,#mermaid-svg-ElGdknNEMG5AyVCl .node polygon,#mermaid-svg-ElGdknNEMG5AyVCl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ElGdknNEMG5AyVCl .rough-node .label text,#mermaid-svg-ElGdknNEMG5AyVCl .node .label text,#mermaid-svg-ElGdknNEMG5AyVCl .image-shape .label,#mermaid-svg-ElGdknNEMG5AyVCl .icon-shape .label{text-anchor:middle;}#mermaid-svg-ElGdknNEMG5AyVCl .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ElGdknNEMG5AyVCl .rough-node .label,#mermaid-svg-ElGdknNEMG5AyVCl .node .label,#mermaid-svg-ElGdknNEMG5AyVCl .image-shape .label,#mermaid-svg-ElGdknNEMG5AyVCl .icon-shape .label{text-align:center;}#mermaid-svg-ElGdknNEMG5AyVCl .node.clickable{cursor:pointer;}#mermaid-svg-ElGdknNEMG5AyVCl .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ElGdknNEMG5AyVCl .arrowheadPath{fill:#333333;}#mermaid-svg-ElGdknNEMG5AyVCl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ElGdknNEMG5AyVCl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ElGdknNEMG5AyVCl .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ElGdknNEMG5AyVCl .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ElGdknNEMG5AyVCl .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ElGdknNEMG5AyVCl .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ElGdknNEMG5AyVCl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ElGdknNEMG5AyVCl .cluster text{fill:#333;}#mermaid-svg-ElGdknNEMG5AyVCl .cluster span{color:#333;}#mermaid-svg-ElGdknNEMG5AyVCl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ElGdknNEMG5AyVCl .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ElGdknNEMG5AyVCl rect.text{fill:none;stroke-width:0;}#mermaid-svg-ElGdknNEMG5AyVCl .icon-shape,#mermaid-svg-ElGdknNEMG5AyVCl .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ElGdknNEMG5AyVCl .icon-shape p,#mermaid-svg-ElGdknNEMG5AyVCl .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ElGdknNEMG5AyVCl .icon-shape .label rect,#mermaid-svg-ElGdknNEMG5AyVCl .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ElGdknNEMG5AyVCl .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ElGdknNEMG5AyVCl .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ElGdknNEMG5AyVCl :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
只读访问
修改数据
存储或转移
消费并返回新值
设计函数参数
函数需要做什么?
用 &T或 &str / &[T]
用 &mut T
用 T获取所有权
T 传入, 新值返回
参数用最宽松的类型
返回值: 借用参数用 &str新建数据用 String
九、多元化练习题(含答案与解读)
🟢 练习 1(基础):借用而非移动
要求:把函数改成借用,让 main 中调用后 s 仍可用。
fn print_str(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
print_str(s);
println!("{}", s); // 想再用
}
参考答案
fn print_str(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
print_str(&s);
println!("{}", s); // ✅
}
解读:参数用 &str 比 &String 更好。&String 会自动解引用成 &str,所以传 &s 合法。参数类型尽量用“最低要求”的类型,这样调用方更灵活。这是 Rust API 设计的核心原则。
🟢 练习 2(可变引用):修复代码
fn add_world(s: &String) {
s.push_str(" world");
}
参考答案
fn add_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // hello world
}
解读:要修改就得用 &mut。三处配合:原变量 mut,调用处 &mut s,参数声明 &mut String。缺一不可。参数也可以写成 &mut str,但 str 是定长的,push_str 需要 String,所以用 &mut String。
🟡 练习 3(借用规则):预测能否编译
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{} {} {}", r1, r2, r3);
}
参考答案
无法编译。
报错:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
解读:r1、r2 两个不可变引用在 println! 里仍然活跃,此时 r3 = &mut s 冲突。修改方法:先打印 r1、r2,再创建 r3,或用 NLL 让 r1、r2 提前结束:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2); // r1、r2 生命周期结束
let r3 = &mut s; // ✅
println!("{}", r3);
🟡 练习 4(NLL):这个能编译吗?
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s;
r2.push_str(" world");
println!("{}", r2);
}
参考答案
能编译。
解读:r1 在 println!("{}", r1) 之后不再使用,生命周期结束。NLL 让编译器知道“r1 死了”,所以 r2 = &mut s 不冲突。这是 Rust 2018 之后的特性,看的是实际使用范围,不是花括号范围。
🟡 练习 5(切片):提取子串
要求:给定 let s = String::from("hello world");,提取 "world" 并打印。
参考答案
fn main() {
let s = String::from("hello world");
let world = &s[6..11];
println!("{}", world);
}
解读:切片语法 &s[start..end],左闭右开。"hello" 是 5 字节,空格 1 字节,所以 "world" 从索引 6 开始。也可以用 &s[6..] 表示从 6 到末尾。切片是引用,不复制数据,非常高效。
🔴 练习 6(函数返回切片):找出错误
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
s
}
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("{}", word);
}
这段能编译吗?如果能,输出什么?
参考答案
能编译,输出 hello。
解读:函数接收 &str 返回 &str,生命周期由编译器自动推断为“返回值与输入参数同寿命”。&s[..i] 返回第一个空格前的切片。i 是空格的位置,所以返回 "hello"。注意这个函数签名 fn first_word(s: &str) -> &str 是 Rust 官方书里的经典例子,生命周期省略规则让它不用手写 'a。
🔴 练习 7(综合):找最大值
要求:写函数 fn max(slice: &[i32]) -> i32,返回切片中的最大值。
参考答案
fn max(slice: &[i32]) -> i32 {
let mut m = slice[0];
for &x in slice {
if x > m {
m = x;
}
}
m
}
fn main() {
let arr = [3, 7, 2, 9, 5];
println!("{}", max(&arr)); // 9
let v = vec![100, 20, 30];
println!("{}", max(&v)); // 100
}
解读:参数用 &[i32] 切片,数组和 Vec 都能传。for &x in slice 中 &x 是模式匹配,解构出 i32(Copy 类型)。m 需要 mut,因为循环中会更新。用 &[i32] 而不是 &[i32; 5],通用性更强。
🔴 练习 8(结构体与借用):判断能否编译
struct Holder {
data: &String,
}
fn main() {
let s = String::from("hello");
let h = Holder { data: &s };
println!("{}", h.data);
}
参考答案
无法编译。
报错:
error[E0106]: missing lifetime specifier
解读:结构体持有引用时,必须标注生命周期,告诉编译器“这个引用和结构体本身的关系”:
struct Holder<'a> {
data: &'a String,
}
'a 表示 Holder 不能活得比它借用的数据更长。这样编译器就能检查 h 不会在 s 死后还被使用。初学者遇到这个报错,加 <'a> 和 &'a 即可。更好的做法是持有 String(获取所有权),除非确实需要借用。
🔵 思考题 1:为什么 Rust 要限制“可变引用和不可变引用不能共存”?
参考答案
为了防止数据竞争(data race)。
数据竞争发生的条件:两个或以上指针同时访问同一数据,至少一个在写,且没有同步机制。结果不可预测,是并发 bug 的主要来源。
Rust 在编译期就禁止这种模式:要么多个只读(安全),要么一个写(独占)。这样编译器帮你挡住了数据竞争,无需运行时检查。
具体场景:
假设允许同时读写,可能出现:
let mut v = vec![1, 2, 3];
let r = &v[0]; // 假设允许
v.push(4); // push 可能重新分配内存
println!("{}", r); // r 指向已释放的内存!
push 可能触发扩容,重新分配堆内存,旧的引用 r 就悬垂了。Rust 禁止这种模式,从根本上杜绝。
解读:这是 Rust “无畏并发”(fearless concurrency)的基石。很多并发 bug 在 Rust 里根本写不出来,因为编译不通过。代价是学习借用规则,收益是大量 bug 消失。
🔵 思考题 2:&String 和 &str 有什么区别?为什么函数参数推荐 &str?
参考答案
&String 是指向 String 的引用,必须来自一个 String 变量。&str 是字符串切片,可以来自 String、字符串字面量、或另一个切片。
推荐 &str 的原因:更通用。&String 能自动解引用成 &str,所以传 &String 给 &str 参数是合法的。反过来不行——字符串字面量是 &str,无法传给 &String 参数。
fn f(s: &str) { }
let s = String::from("hi");
f(&s); // ✅ &String 自动转 &str
f("hi"); // ✅ 字面量本来就是 &str
f(&s[0..1]); // ✅ 切片也是 &str
如果参数是 &String:
fn g(s: &String) { }
g(&s); // ✅
g("hi"); // ❌ 类型不匹配
解读:这是 Rust 的 Deref 强制转换。函数参数尽量用“最低要求”的类型,比如 &str 而非 &String,&[T] 而非 &Vec<T>。这样调用方更灵活,也不用关心传入的是字面量、String 还是切片。这是 Rust API 设计的核心原则之一。
🔵 思考题 3:切片会复制数据吗?
参考答案
不会。
切片只是一个“胖指针”,包含两个信息:指向数据的指针,以及长度。它不复制底层数据,只是借用一段范围。
let s = String::from("hello world");
let w = &s[6..11]; // w 只是指向 s 中第 6 到 11 字节,不复制
内存上:
栈 堆
┌──────────────┐
│ s │
│ ptr ────────┼──────► ┌─────────────────┐
│ len: 11 │ │ "hello world" │
│ cap: 11 │ └─────────────────┘
└──────────────┘
┌──────────────┐
│ w │
│ ptr ────────┼──────► s 的第 6 字节
│ len: 5 │
└──────────────┘
解读:切片的零复制特性让字符串处理非常高效。&s[..] 和 s.clone() 完全不同:前者是借用整个字符串,后者是复制堆数据。切片的代价只有 16 字节(64 位机上指针加长度),无论原数据多大。
🔵 思考题 4:为什么 first_word 返回 &str 而不是 String?
参考答案
返回 &str 不复制数据,性能更好。
fn first_word(s: &str) -> &str { … }
返回的 &str 借用原字符串的一段,零开销。调用方拿到的是引用,不拥有数据。
如果返回 String:
fn first_word(s: &str) -> String {
// 需要复制一段出来
s[..i].to_string()
}
每次调用都分配一块新内存,复制数据。对于“取子串”这种高频操作,开销明显。
但返回 &str 有约束:返回值必须来自参数(或 'static 数据)。因为编译器需要知道返回的引用跟着谁。first_word 返回的切片来自 s,所以生命周期跟着 s。
解读:这是 Rust “借用优先”哲学的体现。能不复制就不复制。标准库的字符串方法大多是“返回切片”或“返回 Option<&str>”,极少复制。只有在确实需要独立数据时才返回 String。
🔵 思考题 5:&mut T 和 T 在函数参数中怎么选?
参考答案
用 &mut T:函数需要修改数据,但不想拿走所有权,调用方后续还要用。
fn append(s: &mut String) {
s.push_str("!");
}
let mut s = String::from("hi");
append(&mut s);
println!("{}", s); // hi!,s 仍可用
用 T:函数需要消费数据,或把数据存起来(比如存到结构体、发送到线程)。
fn store(s: String) {
// 把 s 存到某个地方
}
let s = String::from("hi");
store(s);
// s 不能再用
判断标准:
- 函数内部改完,调用方还要用 → &mut T
- 函数内部改完,调用方不再用,或函数要存起来 → T
- 函数返回新值,旧的就没用了 → T 传入,新值返回
解读:&mut T 是“借出去改一下还回来”,T 是“送出去不还了”。前者更常见,因为大多数函数只是临时操作数据。设计 API 时,优先考虑借用,确实需要所有权再传 T。
🔵 思考题 6:为什么说“引用活到它最后一次被使用”?
参考答案
这是 NLL(非词法生命周期)的核心规则。
在 Rust 2018 之前,引用的生命周期是词法作用域:从声明处到所在花括号结束。这导致很多合法的代码被误判为冲突:
let mut s = String::from("hello");
let r = &s;
println!("{}", r); // 最后一次使用
// 按旧规则,r 活到函数结束
let r2 = &mut s; // 旧规则:❌ 冲突
Rust 2018 之后,编译器做活跃性分析:如果引用在某个点之后再也不会被使用,它就“死了”,不再参与借用检查。
这更符合程序员的直觉:用完就结束,而不是“声明在哪就活到哪”。它让很多常见模式(先读一遍,再改)自然通过编译。
解读:NLL 是 Rust 编译器工程的重要进步。它没有放宽安全性(所有检查依然严格),只是让“生命周期”的概念更精确。理解 NLL,就能理解为什么有些看起来冲突的代码能编译,有些看起来没冲突的却报错。
十、知识点总结(第5课)
引用:&T 借用数据,不获取所有权。指针指向数据,自动解引用。
不可变引用:可以同时存在多个,只读。
可变引用:&mut T,同一时刻只能有一个,且不能与不可变引用共存。
借用规则:同一时刻,要么多个 &T,要么一个 &mut T。共享不可变,独占可变。
NLL:引用的生命周期看实际使用范围,不看作用域范围。
悬垂引用:不能返回局部变量的引用,要么返回所有权,要么返回借用参数。
字符串切片:&str,字符串字面量本身就是 &str。
数组切片:&[T],比 &[T; N] 更通用,数组和 Vec 都能传。
&str vs &String:&str 更通用,优先用 &str 作为参数。
切片是胖指针:指针加长度,16 字节,不复制数据。
Deref 强制转换:&String 自动转 &str,&Vec<T> 自动转 &[T]。
函数参数惯例:用 &str 而非 &String,用 &[T] 而非 &Vec<T>。
结构体持有引用:需要生命周期标注 <'a>。
借用检查的实用性:先写所有权版本,靠编译器提示逐步加 &,比一开始就纠结更高效。
十一、下一课预告
第6课:结构体与方法
- 定义结构体 struct
- 实例化与字段访问
- impl 块与方法
- self、&self、&mut self
- 关联函数与 :: 调用
📌 本课心法:
借用的本质是“临时访问权”。所有权回答“谁拥有”,借用回答“谁在看”。同一时刻要么大家都能看,要么一个人能改。规则简单,威力巨大——它让数据竞争在编译期就无处遁形。真正理解借用的标志是:看到一段代码,你能立刻说出“这个引用活到哪一行”。
网硕互联帮助中心



评论前必须登录!
注册