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

目标:深入理解 &T、&mut T、借用规则背后的设计哲学,掌握悬垂引用、NLL、切片 &str 与 &[T],并能用借用思维重构代码。


目录

  • 一、从所有权的痛点说起
  • 二、引用的本质
    • [1. 引用是什么](#1. 引用是什么)
    • [2. 借用的心智模型](#2. 借用的心智模型)
    • [3. 自动解引用](#3. 自动解引用)
    • [4. 引用不是"弱化版指针"](#4. 引用不是"弱化版指针")
  • [三、不可变引用 &T](#三、不可变引用 &T)
    • [1. 可以同时有多个](#1. 可以同时有多个)
    • [2. 不能通过它修改](#2. 不能通过它修改)
    • [3. 函数参数中的不可变引用](#3. 函数参数中的不可变引用)
    • [4. 引用的作用域](#4. 引用的作用域)
  • [四、可变引用 &mut T](#四、可变引用 &mut T)
    • [1. 基本用法](#1. 基本用法)
    • [2. 同一时刻只能有一个可变引用](#2. 同一时刻只能有一个可变引用)
    • [3. 可变引用与不可变引用不能共存](#3. 可变引用与不可变引用不能共存)
    • [4. NLL:非词法生命周期](#4. NLL:非词法生命周期)
  • 五、借用规则的精确定义
  • 六、悬垂引用
    • [1. 什么是悬垂引用](#1. 什么是悬垂引用)
    • [2. 正确做法一:返回所有权](#2. 正确做法一:返回所有权)
    • [3. 正确做法二:返回传入引用的切片](#3. 正确做法二:返回传入引用的切片)
    • [4. 一个更隐蔽的例子](#4. 一个更隐蔽的例子)
  • 七、切片:引用的一段连续内存
    • [1. 字符串切片 &str](#1. 字符串切片 &str)
    • [2. 字符串字面量就是 &str](#2. 字符串字面量就是 &str)
    • [3. &str vs &String](#3. &str vs &String)
    • [4. 数组切片 &[T]](#4. 数组切片 &[T])
    • [5. 切片是"胖指针"](#5. 切片是"胖指针")
  • 八、借用与函数设计
    • [1. 参数用借用还是所有权?](#1. 参数用借用还是所有权?)
    • [2. 返回借用还是新值?](#2. 返回借用还是新值?)
    • [3. 结构体持有引用需要生命周期标注](#3. 结构体持有引用需要生命周期标注)
    • [4. 实际项目中的借用策略](#4. 实际项目中的借用策略)
  • 九、多元化练习题(含答案与解读)
  • 十、知识点总结(第5课)
  • 十一、下一课预告

一、从所有权的痛点说起

第4课我们知道,把 String 传给函数会发生移动:

rust 复制代码
fn main() {
    let s = String::from("hello");
    print_str(s);
    println!("{}", s);   // ❌ s 已被移动
}

fn print_str(s: String) {
    println!("{}", s);
}

有两种笨办法。

办法一:函数把所有权还回来

rust 复制代码
fn print_str(s: String) -> String {
    println!("{}", s);
    s                    // 还回去
}

fn main() {
    let s = String::from("hello");
    let s = print_str(s);   // 重新接收
    println!("{}", s);
}

能用,但每个函数都要"借了还",代码噪音大。

办法二:克隆一份

rust 复制代码
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)是一个指向某块数据的指针,但它不拥有那块数据。

rust 复制代码
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 在方法调用和字段访问时会自动解引用:

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: String

ptr | len | cap
r: &String

ptr
hello

三、不可变引用 &T

1. 可以同时有多个

不可变引用是只读的。多个人同时读同一本书没问题,所以可以共存:

rust 复制代码
fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    let r3 = &s;
    println!("{} {} {}", r1, r2, r3);   // ✅
}

2. 不能通过它修改

rust 复制代码
fn main() {
    let s = String::from("hello");
    let r = &s;
    // r.push_str(" world");   // ❌ 不能通过不可变引用修改
}

3. 函数参数中的不可变引用

rust 复制代码
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 规则):

rust 复制代码
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:

rust 复制代码
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. 同一时刻只能有一个可变引用

rust 复制代码
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. 可变引用与不可变引用不能共存

rust 复制代码
fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;      // ❌
    println!("{} {}", r1, r2);
}

同理:有人正在读,就不能有人同时写。

4. NLL:非词法生命周期

关键点:Rust 判断借用冲突看的是实际使用范围,不是花括号范围。

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)是指引用指向的内存已经被释放:

rust 复制代码
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. 正确做法一:返回所有权

rust 复制代码
fn no_dangle() -> String {
    let s = String::from("hello");
    s      // 移动所有权出去
}

3. 正确做法二:返回传入引用的切片

rust 复制代码
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. 一个更隐蔽的例子

rust 复制代码
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

切片是对集合中一段连续元素的引用,不复制数据。

rust 复制代码
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

rust 复制代码
let s: &str = "hello";

字符串字面量存在程序的二进制里(静态区),s 是指向它的引用。类型 &'static str,'static 表示和程序同寿命。

3. &str vs &String

&String :指向 String 的引用,必须来自一个 String 变量。

&str :字符串切片,可以来自 String、字符串字面量、另一个切片。

rust 复制代码
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]

rust 复制代码
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:

rust 复制代码
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 传入,新值返回
rust 复制代码
fn read_only(s: &str) { }              // 只读
fn modify(s: &mut String) { }          // 修改
fn store(s: String) { }                // 拿走所有权

2. 返回借用还是新值?

返回借用:借用参数的一部分,调用方仍有所有权。

rust 复制代码
fn first_word(s: &str) -> &str { ... }

返回新值:函数内部创建数据,所有权转给调用方。

rust 复制代码
fn make_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

绝不能返回局部变量的引用:

rust 复制代码
fn bad() -> &String {    // ❌
    let s = String::from("hi");
    &s
}

3. 结构体持有引用需要生命周期标注

rust 复制代码
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 仍可用。

rust 复制代码
fn print_str(s: String) {
    println!("{}", s);
}

fn main() {
    let s = String::from("hello");
    print_str(s);
    println!("{}", s);  // 想再用
}

参考答案

rust 复制代码
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(可变引用):修复代码

rust 复制代码
fn add_world(s: &String) {
    s.push_str(" world");
}

参考答案

rust 复制代码
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(借用规则):预测能否编译

rust 复制代码
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 提前结束:

rust 复制代码
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):这个能编译吗?

rust 复制代码
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" 并打印。
参考答案

rust 复制代码
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(函数返回切片):找出错误

rust 复制代码
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,返回切片中的最大值。
参考答案

rust 复制代码
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(结构体与借用):判断能否编译

rust 复制代码
struct Holder {
    data: &String,
}

fn main() {
    let s = String::from("hello");
    let h = Holder { data: &s };
    println!("{}", h.data);
}

参考答案

无法编译。

报错:

复制代码
error[E0106]: missing lifetime specifier

解读:结构体持有引用时,必须标注生命周期,告诉编译器"这个引用和结构体本身的关系":

rust 复制代码
struct Holder<'a> {
    data: &'a String,
}

'a 表示 Holder 不能活得比它借用的数据更长。这样编译器就能检查 h 不会在 s 死后还被使用。初学者遇到这个报错,加 <'a> 和 &'a 即可。更好的做法是持有 String(获取所有权),除非确实需要借用。


🔵 思考题 1:为什么 Rust 要限制"可变引用和不可变引用不能共存"?

参考答案

为了防止数据竞争(data race)。

数据竞争发生的条件:两个或以上指针同时访问同一数据,至少一个在写,且没有同步机制。结果不可预测,是并发 bug 的主要来源。

Rust 在编译期就禁止这种模式:要么多个只读(安全),要么一个写(独占)。这样编译器帮你挡住了数据竞争,无需运行时检查。

具体场景:

假设允许同时读写,可能出现:

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 参数。

rust 复制代码
fn f(s: &str) { }

let s = String::from("hi");
f(&s);       // ✅ &String 自动转 &str
f("hi");     // ✅ 字面量本来就是 &str
f(&s[0..1]); // ✅ 切片也是 &str

如果参数是 &String:

rust 复制代码
fn g(s: &String) { }

g(&s);       // ✅
g("hi");     // ❌ 类型不匹配

解读 :这是 Rust 的 Deref 强制转换。函数参数尽量用"最低要求"的类型,比如 &str 而非 &String,&[T] 而非 &Vec<T>。这样调用方更灵活,也不用关心传入的是字面量、String 还是切片。这是 Rust API 设计的核心原则之一。


🔵 思考题 3:切片会复制数据吗?

参考答案

不会。

切片只是一个"胖指针",包含两个信息:指向数据的指针,以及长度。它不复制底层数据,只是借用一段范围。

rust 复制代码
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 不复制数据,性能更好。

rust 复制代码
fn first_word(s: &str) -> &str { ... }

返回的 &str 借用原字符串的一段,零开销。调用方拿到的是引用,不拥有数据。

如果返回 String:

rust 复制代码
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:函数需要修改数据,但不想拿走所有权,调用方后续还要用。

rust 复制代码
fn append(s: &mut String) {
    s.push_str("!");
}

let mut s = String::from("hi");
append(&mut s);
println!("{}", s);   // hi!,s 仍可用

用 T:函数需要消费数据,或把数据存起来(比如存到结构体、发送到线程)。

rust 复制代码
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 之前,引用的生命周期是词法作用域:从声明处到所在花括号结束。这导致很多合法的代码被误判为冲突:

rust 复制代码
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
  • 关联函数与 :: 调用

📌 本课心法:

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

相关推荐
qetfw1 小时前
Bash 变量:export、位置参数与默认值展开验证
linux·开发语言·chrome·bash
电子云与长程纠缠1 小时前
UE5魂系游戏开发教程05
开发语言·学习·游戏·ui·ue5
cspttty2 小时前
2027风险管理岗准备路线:数学与应用数学如何补齐金融、SQL与Python
开发语言·python·金融
我命由我123452 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
严谨的麻辣烫2 小时前
Python HTTP 连接池怎么用?从连接复用到请求异常排查
开发语言·python·http·requests·网络排查
码林鼠2 小时前
flutter状态管理入门教程-Riverpod
开发语言·javascript·flutter
system_error_x3 小时前
C++ 编程基础入门教程
开发语言·c++
Amos_Web3 小时前
Markdonw语法
开发语言·前端·javascript
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 回顾和今日内容介绍
java·开发语言·交友