云计算百科
云计算领域专业知识百科平台

【Rust入门练中学】第5课:引用与借用

目标:深入理解 &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
  • 关联函数与 :: 调用

📌 本课心法:

借用的本质是“临时访问权”。所有权回答“谁拥有”,借用回答“谁在看”。同一时刻要么大家都能看,要么一个人能改。规则简单,威力巨大——它让数据竞争在编译期就无处遁形。真正理解借用的标志是:看到一段代码,你能立刻说出“这个引用活到哪一行”。

赞(0)
未经允许不得转载:网硕互联帮助中心 » 【Rust入门练中学】第5课:引用与借用
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!