【Rust入门练中学】第9课:集合类型

目标:掌握 Vec<T>、String、HashMap<K, V> 的常用操作,理解它们的内存布局、所有权行为、性能特征,以及迭代器入门。

目录

  • 一、从数组的局限说起
  • 二、Vec<T>:动态数组
    • [1. 创建](#1. 创建)
    • [2. 内存布局](#2. 内存布局)
    • [3. 增删改查](#3. 增删改查)
    • [4. 遍历](#4. 遍历)
    • [5. 存储不同类型的元素](#5. 存储不同类型的元素)
  • [三、String:可增长的 UTF-8 字符串](#三、String:可增长的 UTF-8 字符串)
    • [1. String vs &str](#1. String vs &str)
    • [2. 创建](#2. 创建)
    • [3. 拼接](#3. 拼接)
    • [4. 不能索引](#4. 不能索引)
    • [5. 常用方法](#5. 常用方法)
    • [6. 字符串与 UTF-8](#6. 字符串与 UTF-8)
  • [四、HashMap<K, V>:键值对](#四、HashMap<K, V>:键值对)
    • [1. 创建与插入](#1. 创建与插入)
    • [2. 查询](#2. 查询)
    • [3. 遍历](#3. 遍历)
    • [4. 更新](#4. 更新)
    • [5. entry API 详解](#5. entry API 详解)
    • [6. 所有权](#6. 所有权)
  • 五、迭代器入门
    • [1. 三种迭代方式](#1. 三种迭代方式)
    • [2. 迭代器是惰性的](#2. 迭代器是惰性的)
    • [3. 常用适配器](#3. 常用适配器)
    • [4. 常用消费器](#4. 常用消费器)
    • [5. 迭代器链](#5. 迭代器链)
  • 六、多元化练习题(含答案与解读)
  • 七、知识点总结(第9课)
  • 八、下一课预告


一、从数组的局限说起

第3课我们学过数组:

rust 复制代码
let arr = [1, 2, 3, 4, 5];

数组的问题是长度固定 。声明时确定,之后不能增删。但实际程序里,数据量往往运行时才知道------读文件、收网络请求、用户输入。这时就需要集合类型。

Rust 标准库提供三大常用集合:

  • Vec<T>:动态数组,堆上分配,长度可变
  • String:可增长的 UTF-8 字符串
  • HashMap<K, V>:键值对哈希表

三者的共同点:数据存在堆上,栈上只存元信息。这意味着它们都有所有权、借用、移动的语义------第4、5课学的规则在这里全部适用。

集合类型是所有权规则最常被触发的地方。 理解了集合,才算真正理解了所有权。


下面是本课三大集合类型的总览图:
#mermaid-svg-c8GosIl0oe6mLs86{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-c8GosIl0oe6mLs86 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c8GosIl0oe6mLs86 .error-icon{fill:#552222;}#mermaid-svg-c8GosIl0oe6mLs86 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c8GosIl0oe6mLs86 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c8GosIl0oe6mLs86 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c8GosIl0oe6mLs86 .marker.cross{stroke:#333333;}#mermaid-svg-c8GosIl0oe6mLs86 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c8GosIl0oe6mLs86 p{margin:0;}#mermaid-svg-c8GosIl0oe6mLs86 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c8GosIl0oe6mLs86 .cluster-label text{fill:#333;}#mermaid-svg-c8GosIl0oe6mLs86 .cluster-label span{color:#333;}#mermaid-svg-c8GosIl0oe6mLs86 .cluster-label span p{background-color:transparent;}#mermaid-svg-c8GosIl0oe6mLs86 .label text,#mermaid-svg-c8GosIl0oe6mLs86 span{fill:#333;color:#333;}#mermaid-svg-c8GosIl0oe6mLs86 .node rect,#mermaid-svg-c8GosIl0oe6mLs86 .node circle,#mermaid-svg-c8GosIl0oe6mLs86 .node ellipse,#mermaid-svg-c8GosIl0oe6mLs86 .node polygon,#mermaid-svg-c8GosIl0oe6mLs86 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c8GosIl0oe6mLs86 .rough-node .label text,#mermaid-svg-c8GosIl0oe6mLs86 .node .label text,#mermaid-svg-c8GosIl0oe6mLs86 .image-shape .label,#mermaid-svg-c8GosIl0oe6mLs86 .icon-shape .label{text-anchor:middle;}#mermaid-svg-c8GosIl0oe6mLs86 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c8GosIl0oe6mLs86 .rough-node .label,#mermaid-svg-c8GosIl0oe6mLs86 .node .label,#mermaid-svg-c8GosIl0oe6mLs86 .image-shape .label,#mermaid-svg-c8GosIl0oe6mLs86 .icon-shape .label{text-align:center;}#mermaid-svg-c8GosIl0oe6mLs86 .node.clickable{cursor:pointer;}#mermaid-svg-c8GosIl0oe6mLs86 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c8GosIl0oe6mLs86 .arrowheadPath{fill:#333333;}#mermaid-svg-c8GosIl0oe6mLs86 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c8GosIl0oe6mLs86 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c8GosIl0oe6mLs86 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8GosIl0oe6mLs86 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c8GosIl0oe6mLs86 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8GosIl0oe6mLs86 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c8GosIl0oe6mLs86 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c8GosIl0oe6mLs86 .cluster text{fill:#333;}#mermaid-svg-c8GosIl0oe6mLs86 .cluster span{color:#333;}#mermaid-svg-c8GosIl0oe6mLs86 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-c8GosIl0oe6mLs86 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c8GosIl0oe6mLs86 rect.text{fill:none;stroke-width:0;}#mermaid-svg-c8GosIl0oe6mLs86 .icon-shape,#mermaid-svg-c8GosIl0oe6mLs86 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c8GosIl0oe6mLs86 .icon-shape p,#mermaid-svg-c8GosIl0oe6mLs86 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c8GosIl0oe6mLs86 .icon-shape .label rect,#mermaid-svg-c8GosIl0oe6mLs86 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c8GosIl0oe6mLs86 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c8GosIl0oe6mLs86 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c8GosIl0oe6mLs86 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Rust 集合类型
Vec 动态数组
String 可增长字符串
HashMap 键值对
堆上存储,长度可变
UTF-8 编码,可增长
键值映射,哈希表
共同点:数据在堆上,栈上只存元信息
所有权、借用、移动语义全部适用

二、Vec<T>:动态数组

1. 创建

rust 复制代码
let v1: Vec<i32> = Vec::new();       // 空
let v2 = vec![1, 2, 3];              // 宏,带初始值
let v3 = vec![0; 5];                 // [0, 0, 0, 0, 0]

Vec::new() 需要类型标注,因为编译器不知道装什么。vec![] 宏通过初始值推断类型。

2. 内存布局

rust 复制代码
let v = vec![1, 2, 3];
复制代码
栈                          堆
┌──────────────┐
│ v            │
│  ptr ────────┼──────► ┌───┬───┬───┐
│  len: 3      │        │ 1 │ 2 │ 3 │
│  cap: 3      │        └───┴───┴───┘
└──────────────┘

和 String 一样,Vec 在栈上存三要素:指针、长度、容量。

长度(len):当前有几个元素。

容量(cap):已分配的空间能装几个元素。

为什么区分? 因为扩容昂贵。push 时如果 len == cap,需要重新分配更大的堆空间,复制旧数据,释放旧内存。为了减少扩容次数,Vec 通常会预留额外空间(容量翻倍策略)。

rust 复制代码
let mut v = Vec::new();
println!("{}", v.capacity());   // 0
v.push(1);
println!("{}", v.capacity());   // 4(第一次分配,通常给 4)
v.push(2);
v.push(3);
v.push(4);
v.push(5);
println!("{}", v.capacity());   // 8(满了翻倍)

性能建议 :如果知道大概元素数量,用 Vec::with_capacity(n) 预分配,避免多次扩容:

rust 复制代码
let mut v = Vec::with_capacity(1000);   // 一次分配够
for i in 0..1000 {
    v.push(i);   // 不会触发扩容
}

下面是 Vec 扩容过程的流程图:
#mermaid-svg-MnNxdM81wBY3okWQ{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-MnNxdM81wBY3okWQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MnNxdM81wBY3okWQ .error-icon{fill:#552222;}#mermaid-svg-MnNxdM81wBY3okWQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MnNxdM81wBY3okWQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MnNxdM81wBY3okWQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MnNxdM81wBY3okWQ .marker.cross{stroke:#333333;}#mermaid-svg-MnNxdM81wBY3okWQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MnNxdM81wBY3okWQ p{margin:0;}#mermaid-svg-MnNxdM81wBY3okWQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-MnNxdM81wBY3okWQ .cluster-label text{fill:#333;}#mermaid-svg-MnNxdM81wBY3okWQ .cluster-label span{color:#333;}#mermaid-svg-MnNxdM81wBY3okWQ .cluster-label span p{background-color:transparent;}#mermaid-svg-MnNxdM81wBY3okWQ .label text,#mermaid-svg-MnNxdM81wBY3okWQ span{fill:#333;color:#333;}#mermaid-svg-MnNxdM81wBY3okWQ .node rect,#mermaid-svg-MnNxdM81wBY3okWQ .node circle,#mermaid-svg-MnNxdM81wBY3okWQ .node ellipse,#mermaid-svg-MnNxdM81wBY3okWQ .node polygon,#mermaid-svg-MnNxdM81wBY3okWQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MnNxdM81wBY3okWQ .rough-node .label text,#mermaid-svg-MnNxdM81wBY3okWQ .node .label text,#mermaid-svg-MnNxdM81wBY3okWQ .image-shape .label,#mermaid-svg-MnNxdM81wBY3okWQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-MnNxdM81wBY3okWQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MnNxdM81wBY3okWQ .rough-node .label,#mermaid-svg-MnNxdM81wBY3okWQ .node .label,#mermaid-svg-MnNxdM81wBY3okWQ .image-shape .label,#mermaid-svg-MnNxdM81wBY3okWQ .icon-shape .label{text-align:center;}#mermaid-svg-MnNxdM81wBY3okWQ .node.clickable{cursor:pointer;}#mermaid-svg-MnNxdM81wBY3okWQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MnNxdM81wBY3okWQ .arrowheadPath{fill:#333333;}#mermaid-svg-MnNxdM81wBY3okWQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MnNxdM81wBY3okWQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MnNxdM81wBY3okWQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnNxdM81wBY3okWQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MnNxdM81wBY3okWQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnNxdM81wBY3okWQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MnNxdM81wBY3okWQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MnNxdM81wBY3okWQ .cluster text{fill:#333;}#mermaid-svg-MnNxdM81wBY3okWQ .cluster span{color:#333;}#mermaid-svg-MnNxdM81wBY3okWQ 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-MnNxdM81wBY3okWQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MnNxdM81wBY3okWQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-MnNxdM81wBY3okWQ .icon-shape,#mermaid-svg-MnNxdM81wBY3okWQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MnNxdM81wBY3okWQ .icon-shape p,#mermaid-svg-MnNxdM81wBY3okWQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MnNxdM81wBY3okWQ .icon-shape .label rect,#mermaid-svg-MnNxdM81wBY3okWQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MnNxdM81wBY3okWQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MnNxdM81wBY3okWQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MnNxdM81wBY3okWQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
push 新元素
len == cap ?
直接写入,O(1)
重新分配更大的堆空间
复制旧数据到新空间
释放旧内存
容量翻倍(如 4 → 8)

3. 增删改查

rust 复制代码
let mut v = vec![1, 2, 3];

v.push(4);              // 尾部添加,均摊 O(1)
v.pop();                 // 弹出尾部,返回 Option<T>,O(1)
v.insert(0, 0);          // 在索引 0 插入,O(n),后面元素后移
v.remove(0);             // 删除索引 0,返回被删元素,O(n)

let first = &v[0];       // 索引访问,越界 panic
let second = v.get(1);   // 返回 Option<&T>,越界返回 None

println!("{}", v.len()); // 长度

[] 和 .get() 的区别:

  • v[0] 越界直接 panic。适合"我确定索引有效"的场景。
  • v.get(0) 返回 Option<&T>。适合"索引可能无效"的场景。
rust 复制代码
let v = vec![1, 2, 3];
let x = v.get(10);   // None,不会 panic
if let Some(x) = x {
    println!("{}", x);
}

和 String 一样,Vec 也不能随意在借用时修改:

rust 复制代码
let mut v = vec![1, 2, 3];
let first = &v[0];      // 借用第一个元素
v.push(4);              // ❌ 不能在有借用时修改
println!("{}", first);

为什么?push 可能触发扩容,重新分配堆内存,first 就悬垂了。Rust 借用检查器挡住了这个问题。这就是第5课借用规则的实际应用。

4. 遍历

rust 复制代码
let v = vec![1, 2, 3];

// 不可变借用
for x in &v {
    println!("{}", x);   // x: &i32
}

// 可变借用
let mut v2 = vec![1, 2, 3];
for x in &mut v2 {
    *x *= 2;             // 解引用后修改
}

// 获取所有权
for x in v {
    println!("{}", x);   // x: i32,v 被移动
}
// v 不能再用

三种遍历方式对应三种所有权语义,和第5课讲的三种引用形式一一对应。

5. 存储不同类型的元素

Vec<T> 要求所有元素同类型。想存不同类型,用枚举:

rust 复制代码
enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

这是第7课枚举的实际应用。枚举让"不同类型"变成"同一个类型的不同变体"。


三、String:可增长的 UTF-8 字符串

1. String vs &str

String:拥有所有权,堆上分配,可增长。

&str :借用,不可变,可能是字符串字面量或 String 的切片。

rust 复制代码
let s1: String = String::from("hello");   // 拥有
let s2: &str = "hello";                    // 借用(字面量)
let s3: &str = &s1;                        // 借用(String 的引用)

选择原则:

  • 需要拥有、修改 → String
  • 只是读取、传参 → &str
  • 函数参数优先 &str(更通用)

2. 创建

rust 复制代码
let s1 = String::new();                  // 空
let s2 = String::from("hello");          // 从字面量
let s3 = "hello".to_string();            // 同上
let s4 = "hello".to_owned();             // 同上(更通用)
let s5 = String::with_capacity(100);     // 预分配

3. 拼接

rust 复制代码
let mut s = String::from("hello");
s.push(' ');              // 追加字符
s.push_str("world");      // 追加字符串
println!("{}", s);        // hello world

+ 运算符:

rust 复制代码
let s1 = String::from("hello ");
let s2 = String::from("world");
let s3 = s1 + &s2;        // s1 被移动,s2 借用
// println!("{}", s1);    // ❌ s1 已被移动
println!("{}", s3);       // hello world

为什么 s1 被移动? 因为 + 的签名类似:

rust 复制代码
fn add(self, other: &str) -> String

self 是 s1(按值),other 是 &s2(借用)。s1 的所有权被 add 拿走,返回新的 String 赋给 s3。

为什么不直接修改 s1? 因为 add 可以复用 s1 的堆空间,避免新分配。这是性能优化------如果 s1 容量够,直接追加;不够才重新分配。

拼接多个用 format!:

rust 复制代码
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = String::from("c");
let result = format!("{}-{}-{}", s1, s2, s3);   // 都不移动
println!("{}", result);   // a-b-c
println!("{}", s1);       // ✅ s1 还能用

format! 借用所有参数,不移动。这是拼接多个字符串的首选。

下面是 String 拼接方式的选择流程图:
#mermaid-svg-gCbeNYfyI8Hx04F4{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-gCbeNYfyI8Hx04F4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gCbeNYfyI8Hx04F4 .error-icon{fill:#552222;}#mermaid-svg-gCbeNYfyI8Hx04F4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gCbeNYfyI8Hx04F4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .marker.cross{stroke:#333333;}#mermaid-svg-gCbeNYfyI8Hx04F4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gCbeNYfyI8Hx04F4 p{margin:0;}#mermaid-svg-gCbeNYfyI8Hx04F4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster-label text{fill:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster-label span{color:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster-label span p{background-color:transparent;}#mermaid-svg-gCbeNYfyI8Hx04F4 .label text,#mermaid-svg-gCbeNYfyI8Hx04F4 span{fill:#333;color:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .node rect,#mermaid-svg-gCbeNYfyI8Hx04F4 .node circle,#mermaid-svg-gCbeNYfyI8Hx04F4 .node ellipse,#mermaid-svg-gCbeNYfyI8Hx04F4 .node polygon,#mermaid-svg-gCbeNYfyI8Hx04F4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .rough-node .label text,#mermaid-svg-gCbeNYfyI8Hx04F4 .node .label text,#mermaid-svg-gCbeNYfyI8Hx04F4 .image-shape .label,#mermaid-svg-gCbeNYfyI8Hx04F4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-gCbeNYfyI8Hx04F4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .rough-node .label,#mermaid-svg-gCbeNYfyI8Hx04F4 .node .label,#mermaid-svg-gCbeNYfyI8Hx04F4 .image-shape .label,#mermaid-svg-gCbeNYfyI8Hx04F4 .icon-shape .label{text-align:center;}#mermaid-svg-gCbeNYfyI8Hx04F4 .node.clickable{cursor:pointer;}#mermaid-svg-gCbeNYfyI8Hx04F4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .arrowheadPath{fill:#333333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gCbeNYfyI8Hx04F4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gCbeNYfyI8Hx04F4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gCbeNYfyI8Hx04F4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster text{fill:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 .cluster span{color:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 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-gCbeNYfyI8Hx04F4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gCbeNYfyI8Hx04F4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-gCbeNYfyI8Hx04F4 .icon-shape,#mermaid-svg-gCbeNYfyI8Hx04F4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gCbeNYfyI8Hx04F4 .icon-shape p,#mermaid-svg-gCbeNYfyI8Hx04F4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gCbeNYfyI8Hx04F4 .icon-shape .label rect,#mermaid-svg-gCbeNYfyI8Hx04F4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gCbeNYfyI8Hx04F4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gCbeNYfyI8Hx04F4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gCbeNYfyI8Hx04F4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 两个
可移动左边
都要保留
多个
需要拼接字符串
拼接几个?
是否关心所有权?
使用 + 运算符
使用 format!
s1 被移动,s2 借用
所有参数都借用,不移动

4. 不能索引

rust 复制代码
let s = String::from("hello");
// let c = s[0];   // ❌ 编译错误

为什么? 因为 String 是 UTF-8 编码,一个字符可能占 1 到 4 个字节。

rust 复制代码
let s = String::from("中文");
// s[0] 是什么?"中" 占 3 字节,s[0] 只拿到第一个字节

Rust 禁止索引,强制你明确选择:

rust 复制代码
let s = String::from("hello");

// 按字节
for b in s.bytes() {
    println!("{}", b);   // 104, 101, 108, 108, 111
}

// 按字符
for c in s.chars() {
    println!("{}", c);   // h, e, l, l, o
}

// 切片(按字节,必须落在字符边界)
let hello = &s[0..5];

切片边界 :&s[0..3] 对 "中文" 合法("中" 占 3 字节),但 &s[0..1] 会 panic,因为边界不在字符边界。

这是 Rust 对"正确性"的坚持 。其他语言允许 s[0] 返回字节或字符,容易出 bug。Rust 让你明确选择"按字符"还是"按字节",字符串处理是很多 bug 的来源,Rust 在这里格外严格。

5. 常用方法

rust 复制代码
let s = String::from("Hello, World");

s.len();                    // 13(字节数,不是字符数)
s.is_empty();               // false
s.contains("World");        // true
s.starts_with("Hello");     // true
s.to_lowercase();           // "hello, world",返回新 String
s.to_uppercase();           // "HELLO, WORLD"
s.trim();                   // 去掉两端空白,返回 &str
s.replace("World", "Rust"); // "Hello, Rust"
s.split(", ");              // 迭代器,按分隔符切分

len() 返回字节数,不是字符数:

rust 复制代码
let s = String::from("中文");
println!("{}", s.len());           // 6(两个字符各占 3 字节)
println!("{}", s.chars().count()); // 2(字符数)

6. 字符串与 UTF-8

Rust 的 String 永远是合法 UTF-8。这带来两个后果:

第一,不能随意修改字节 。s[0] = 'x' 不合法,因为可能破坏 UTF-8 结构。

第二,索引和切片要小心。切片必须落在字符边界:

rust 复制代码
let s = String::from("hello");
let part = &s[0..3];   // ✅ "hel"

let s2 = String::from("中文");
// let part2 = &s2[0..1];   // ❌ panic: byte index 1 is not a char boundary
let part3 = &s2[0..3];      // ✅ "中"

怎么安全切片? 用 chars() 或 char_indices():

rust 复制代码
let s = String::from("中文");
let first_char: String = s.chars().take(1).collect();   // "中"

四、HashMap<K, V>:键值对

1. 创建与插入

需要 use std::collections::HashMap;。

rust 复制代码
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 90);
scores.insert(String::from("Bob"), 85);

HashMap<K, V> 要求 K: Hash + Eq(能哈希、能比较相等)。String、&str、整数、bool 都满足。浮点不满足(因为 NaN != NaN)。

2. 查询

rust 复制代码
let name = String::from("Alice");
let score = scores.get(&name);   // Option<&i32>

match score {
    Some(s) => println!("{}", s),
    None => println!("没找到"),
}

// 或用 unwrap_or
let s = scores.get(&name).unwrap_or(&0);

get 接收 &K,返回 Option<&V>。用 &name 而不是 name,避免移动。

3. 遍历

rust 复制代码
for (k, v) in &scores {
    println!("{}: {}", k, v);
}

遍历顺序不确定 。HashMap 内部用哈希,元素顺序随机。如果需要有序,用 BTreeMap。

4. 更新

覆盖:

rust 复制代码
scores.insert(String::from("Alice"), 95);   // 旧值被替换

只在键不存在时插入:

rust 复制代码
scores.entry(String::from("Carol")).or_insert(80);

基于旧值更新:

rust 复制代码
let text = "hello world hello rust";
let mut counts = HashMap::new();
for word in text.split_whitespace() {
    let count = counts.entry(word).or_insert(0);
    *count += 1;
}
// {"hello": 2, "world": 1, "rust": 1}

.entry(key) 返回 Entry 枚举,.or_insert(v) 在键不存在时插入 v 并返回 &mut V。这是 HashMap 最经典的用法。

下面是 HashMap 更新策略的选择流程图:
#mermaid-svg-0Lm80LBSrpwX1Ikx{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-0Lm80LBSrpwX1Ikx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0Lm80LBSrpwX1Ikx .error-icon{fill:#552222;}#mermaid-svg-0Lm80LBSrpwX1Ikx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0Lm80LBSrpwX1Ikx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .marker.cross{stroke:#333333;}#mermaid-svg-0Lm80LBSrpwX1Ikx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0Lm80LBSrpwX1Ikx p{margin:0;}#mermaid-svg-0Lm80LBSrpwX1Ikx .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster-label text{fill:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster-label span{color:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster-label span p{background-color:transparent;}#mermaid-svg-0Lm80LBSrpwX1Ikx .label text,#mermaid-svg-0Lm80LBSrpwX1Ikx span{fill:#333;color:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .node rect,#mermaid-svg-0Lm80LBSrpwX1Ikx .node circle,#mermaid-svg-0Lm80LBSrpwX1Ikx .node ellipse,#mermaid-svg-0Lm80LBSrpwX1Ikx .node polygon,#mermaid-svg-0Lm80LBSrpwX1Ikx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .rough-node .label text,#mermaid-svg-0Lm80LBSrpwX1Ikx .node .label text,#mermaid-svg-0Lm80LBSrpwX1Ikx .image-shape .label,#mermaid-svg-0Lm80LBSrpwX1Ikx .icon-shape .label{text-anchor:middle;}#mermaid-svg-0Lm80LBSrpwX1Ikx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .rough-node .label,#mermaid-svg-0Lm80LBSrpwX1Ikx .node .label,#mermaid-svg-0Lm80LBSrpwX1Ikx .image-shape .label,#mermaid-svg-0Lm80LBSrpwX1Ikx .icon-shape .label{text-align:center;}#mermaid-svg-0Lm80LBSrpwX1Ikx .node.clickable{cursor:pointer;}#mermaid-svg-0Lm80LBSrpwX1Ikx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .arrowheadPath{fill:#333333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0Lm80LBSrpwX1Ikx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0Lm80LBSrpwX1Ikx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0Lm80LBSrpwX1Ikx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster text{fill:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx .cluster span{color:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx 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-0Lm80LBSrpwX1Ikx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0Lm80LBSrpwX1Ikx rect.text{fill:none;stroke-width:0;}#mermaid-svg-0Lm80LBSrpwX1Ikx .icon-shape,#mermaid-svg-0Lm80LBSrpwX1Ikx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0Lm80LBSrpwX1Ikx .icon-shape p,#mermaid-svg-0Lm80LBSrpwX1Ikx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0Lm80LBSrpwX1Ikx .icon-shape .label rect,#mermaid-svg-0Lm80LBSrpwX1Ikx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0Lm80LBSrpwX1Ikx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0Lm80LBSrpwX1Ikx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0Lm80LBSrpwX1Ikx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是,直接覆盖
否,才插入
基于旧值更新
需要更新 HashMap
键已存在?
insert(key, new_value)
entry(key).or_insert(value)
entry(key).or_insert(0) 后自增
返回 &mut V,可继续修改

5. entry API 详解

rust 复制代码
use std::collections::HashMap;

let mut map = HashMap::new();

// 方法一:entry + or_insert
map.entry("a").or_insert(0);

// 方法二:entry + or_insert_with(惰性)
map.entry("b").or_insert_with(|| expensive_computation());

// 方法三:entry + or_default
map.entry("c").or_default();   // 用 Default 值

// 方法四:match entry
match map.entry("d") {
    Entry::Occupied(e) => println!("已存在: {}", e.get()),
    Entry::Vacant(e) => { e.insert(0); }
}

or_insert 立即计算默认值,or_insert_with 惰性计算。默认值昂贵时用后者。

6. 所有权

HashMap 拥有键和值:

rust 复制代码
let key = String::from("Alice");
let value = 90;
let mut map = HashMap::new();
map.insert(key, value);
// println!("{}", key);   // ❌ key 被移动

String 类型的键被移动进 HashMap。i32 是 Copy,复制进去。

借用查询不移动:

rust 复制代码
let name = String::from("Alice");
map.insert(name, 90);

let n = String::from("Alice");
let score = map.get(&n);   // 借用,不移动

get 接收 &K,用 &n 查询。n 仍然可用。


五、迭代器入门

迭代器是 Rust 处理序列的核心抽象。Vec、HashMap、String 都能产生迭代器。

1. 三种迭代方式

rust 复制代码
let v = vec![1, 2, 3];

for x in &v { }        // 不可变引用,x: &i32
for x in &mut v { }    // 可变引用,x: &mut i32
for x in v { }         // 获取所有权,x: i32

对应三个方法:

rust 复制代码
v.iter()        // 等价于 &v
v.iter_mut()    // 等价于 &mut v
v.into_iter()   // 等价于 v(消耗 v)

2. 迭代器是惰性的

迭代器适配器不立即执行 ,只有调用消费器(如 collect、sum、for)才真正运行:

rust 复制代码
let v = vec![1, 2, 3, 4, 5];

// 这行不执行任何计算
let iter = v.iter().map(|x| {
    println!("处理 {}", x);
    x * 2
});

// 这行才触发
let doubled: Vec<i32> = iter.collect();
// 输出:处理 1,处理 2,...

惰性带来两个好处:

第一,组合性强 。可以自由拼接 map、filter、take 而不浪费中间计算。

第二,性能好。编译器能优化迭代器链,常常和手写循环一样快。

3. 常用适配器

map:转换每个元素。

rust 复制代码
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
// [2, 4, 6]

filter:保留满足条件的元素。

rust 复制代码
let evens: Vec<&i32> = v.iter().filter(|x| *x % 2 == 0).collect();
// [2]

take / skip:取前 n 个 / 跳过前 n 个。

rust 复制代码
let first_two: Vec<&i32> = v.iter().take(2).collect();
// [1, 2]

chain:连接两个迭代器。

rust 复制代码
let v2 = vec![4, 5];
let all: Vec<&i32> = v.iter().chain(v2.iter()).collect();
// [1, 2, 3, 4, 5]

4. 常用消费器

collect:收集成集合。

rust 复制代码
let v: Vec<i32> = (1..=5).collect();
let set: std::collections::HashSet<i32> = (1..=5).collect();
let s: String = vec!['a', 'b', 'c'].iter().collect();

sum / product:求和 / 求积。

rust 复制代码
let sum: i32 = vec![1, 2, 3].iter().sum();     // 6
let product: i32 = vec![1, 2, 3].iter().product();   // 6

count:计数。

rust 复制代码
let n = vec![1, 2, 3].iter().count();   // 3

max / min:最大 / 最小。

rust 复制代码
let max = vec![1, 5, 3].iter().max();   // Some(&5)
let min = vec![1, 5, 3].iter().min();   // Some(&1)

find:找第一个满足条件的。

rust 复制代码
let first_even = vec![1, 3, 4, 5].iter().find(|x| *x % 2 == 0);   // Some(&4)

any / all:是否存在 / 是否全部。

rust 复制代码
let has_even = vec![1, 3, 4].iter().any(|x| x % 2 == 0);   // true
let all_positive = vec![1, 3, 4].iter().all(|x| *x > 0);   // true

5. 迭代器链

rust 复制代码
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

let result: i32 = v.iter()
    .filter(|x| *x % 2 == 0)     // 保留偶数:2, 4, 6, 8, 10
    .map(|x| x * x)              // 平方:4, 16, 36, 64, 100
    .take(3)                     // 取前 3 个:4, 16, 36
    .sum();                      // 求和:56

println!("{}", result);   // 56

这就是迭代器的威力:声明式、可组合、惰性求值。你只描述"做什么",编译器负责"怎么做"。


下面是迭代器链的完整执行流程图:
#mermaid-svg-VGlMEdGT6IDFXZBN{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-VGlMEdGT6IDFXZBN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VGlMEdGT6IDFXZBN .error-icon{fill:#552222;}#mermaid-svg-VGlMEdGT6IDFXZBN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VGlMEdGT6IDFXZBN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VGlMEdGT6IDFXZBN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VGlMEdGT6IDFXZBN .marker.cross{stroke:#333333;}#mermaid-svg-VGlMEdGT6IDFXZBN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VGlMEdGT6IDFXZBN p{margin:0;}#mermaid-svg-VGlMEdGT6IDFXZBN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster-label text{fill:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster-label span{color:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster-label span p{background-color:transparent;}#mermaid-svg-VGlMEdGT6IDFXZBN .label text,#mermaid-svg-VGlMEdGT6IDFXZBN span{fill:#333;color:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN .node rect,#mermaid-svg-VGlMEdGT6IDFXZBN .node circle,#mermaid-svg-VGlMEdGT6IDFXZBN .node ellipse,#mermaid-svg-VGlMEdGT6IDFXZBN .node polygon,#mermaid-svg-VGlMEdGT6IDFXZBN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VGlMEdGT6IDFXZBN .rough-node .label text,#mermaid-svg-VGlMEdGT6IDFXZBN .node .label text,#mermaid-svg-VGlMEdGT6IDFXZBN .image-shape .label,#mermaid-svg-VGlMEdGT6IDFXZBN .icon-shape .label{text-anchor:middle;}#mermaid-svg-VGlMEdGT6IDFXZBN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VGlMEdGT6IDFXZBN .rough-node .label,#mermaid-svg-VGlMEdGT6IDFXZBN .node .label,#mermaid-svg-VGlMEdGT6IDFXZBN .image-shape .label,#mermaid-svg-VGlMEdGT6IDFXZBN .icon-shape .label{text-align:center;}#mermaid-svg-VGlMEdGT6IDFXZBN .node.clickable{cursor:pointer;}#mermaid-svg-VGlMEdGT6IDFXZBN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VGlMEdGT6IDFXZBN .arrowheadPath{fill:#333333;}#mermaid-svg-VGlMEdGT6IDFXZBN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VGlMEdGT6IDFXZBN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VGlMEdGT6IDFXZBN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VGlMEdGT6IDFXZBN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VGlMEdGT6IDFXZBN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VGlMEdGT6IDFXZBN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster text{fill:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN .cluster span{color:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN 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-VGlMEdGT6IDFXZBN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VGlMEdGT6IDFXZBN rect.text{fill:none;stroke-width:0;}#mermaid-svg-VGlMEdGT6IDFXZBN .icon-shape,#mermaid-svg-VGlMEdGT6IDFXZBN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VGlMEdGT6IDFXZBN .icon-shape p,#mermaid-svg-VGlMEdGT6IDFXZBN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VGlMEdGT6IDFXZBN .icon-shape .label rect,#mermaid-svg-VGlMEdGT6IDFXZBN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VGlMEdGT6IDFXZBN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VGlMEdGT6IDFXZBN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VGlMEdGT6IDFXZBN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始数据 1..10
filter 保留偶数
map 平方
take 取前 3 个
sum 求和
结果 56

六、多元化练习题(含答案与解读)

🟢 练习 1(基础):Vec 操作

要求 :创建 vec![1, 2, 3],push 4,在索引 0 插入 0,打印长度和所有元素。
参考答案

rust 复制代码
fn main() {
    let mut v = vec![1, 2, 3];
    v.push(4);
    v.insert(0, 0);
    println!("长度: {}", v.len());   // 5
    for x in &v {
        println!("{}", x);           // 0 1 2 3 4
    }
}

解读 :push 尾部添加 O(1),insert(0, 0) 在索引 0 插入 O(n)(后面元素后移)。遍历用 &v 借用,不移动。


🟢 练习 2(String 拼接):修复代码

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = String::from(" world");
    let s3 = s1 + s2;
    println!("{} {}", s1, s3);
}

参考答案

rust 复制代码
fn main() {
    let s1 = String::from("hello");
    let s2 = String::from(" world");
    let s3 = s1 + &s2;      // s2 改为借用
    println!("{}", s3);      // s1 已被移动,不能再打印
}

解读 :+ 的左边会移动,右边必须是 &str。s1 在 s1 + &s2 后失效,所以不能 println!("{}", s1)。想都保留用 format!("{}{}", s1, s2)。


🟡 练习 3(HashMap 词频):统计单词

要求 :统计 "the quick brown fox the lazy dog the" 中每个单词出现次数。
参考答案

rust 复制代码
use std::collections::HashMap;

fn main() {
    let text = "the quick brown fox the lazy dog the";
    let mut counts = HashMap::new();

    for word in text.split_whitespace() {
        let count = counts.entry(word).or_insert(0);
        *count += 1;
    }

    for (word, count) in &counts {
        println!("{}: {}", word, count);
    }
}

解读 :entry(word).or_insert(0) 返回 &mut i32,键不存在时插入 0。*count += 1 解引用后自增。这是 HashMap 最经典的用法之一。注意 word 是 &str,HashMap<&str, i32> 的键借用 text,text 活得比 counts 长,安全。


🟡 练习 4(Vec 与 Option):安全访问

要求 :写函数 fn third(v: &[i32]) -> Option<i32>,返回第三个元素。
参考答案

rust 复制代码
fn third(v: &[i32]) -> Option<i32> {
    v.get(2).copied()
}

fn main() {
    println!("{:?}", third(&[1, 2, 3, 4]));   // Some(3)
    println!("{:?}", third(&[1, 2]));         // None
}

解读 :v.get(2) 返回 Option<&i32>,.copied() 把 Option<&i32> 转成 Option<i32>(因为 i32 是 Copy)。也可以用 v.get(2).map(|x| *x)。返回 Option 比返回 -1 更清晰。


🟡 练习 5(迭代器):预测输出

rust 复制代码
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let result: i32 = v.iter()
        .filter(|x| *x % 2 != 0)
        .map(|x| x * 10)
        .sum();
    println!("{}", result);
}

参考答案

输出 90。

解读 :filter 保留奇数 1、3、5,map 各乘 10 得 10、30、50,sum 得 90。迭代器链是惰性的,sum 才真正驱动执行。注意 filter 闭包参数是 &&i32,所以用 *x 解一层。


🟡 练习 6(String 切片):安全提取

要求 :给定 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..]。切片是引用,不复制数据。


🔴 练习 7(综合):学生成绩管理

要求:

  • 用 HashMap<String, Vec<i32>> 存每个学生的成绩
  • 添加 Alice 的 [90, 85, 92],Bob 的 [78, 88]
  • 打印每个学生的平均分

参考答案

rust 复制代码
use std::collections::HashMap;

fn main() {
    let mut grades: HashMap<String, Vec<i32>> = HashMap::new();

    grades.insert(String::from("Alice"), vec![90, 85, 92]);
    grades.insert(String::from("Bob"), vec![78, 88]);

    for (name, scores) in &grades {
        let sum: i32 = scores.iter().sum();
        let avg = sum as f64 / scores.len() as f64;
        println!("{}: {:.1}", name, avg);
    }
}

解读 :HashMap<String, Vec<i32>> 是嵌套集合。scores.iter().sum() 求和,as f64 转浮点再除,避免整数除法截断。{:.1} 保留一位小数。


🔴 练习 8(迭代器与 HashMap):按值排序

要求 :给定 HashMap<&str, i32>,按值从大到小打印键值对。
参考答案

rust 复制代码
use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert("Alice", 90);
    scores.insert("Bob", 85);
    scores.insert("Carol", 95);

    let mut pairs: Vec<(&str, i32)> = scores.into_iter().collect();
    pairs.sort_by(|a, b| b.1.cmp(&a.1));

    for (name, score) in pairs {
        println!("{}: {}", name, score);
    }
}

解读 :HashMap 本身无序,要排序先 collect 成 Vec。into_iter() 获取所有权,元素是 (&str, i32)。sort_by 用比较闭包,b.1.cmp(&a.1) 实现降序。a、b 是 &(&str, i32),a.1 是 i32,需要 &a.1 才能调 cmp。


🔴 练习 9(综合):单词长度统计

要求 :读入一段文本,用迭代器统计每个长度出现了多少个单词。比如 "a bb ccc dd",输出 1: 1, 2: 2, 3: 1。
参考答案

rust 复制代码
use std::collections::HashMap;

fn main() {
    let text = "a bb ccc dd";
    let mut counts: HashMap<usize, usize> = HashMap::new();

    for word in text.split_whitespace() {
        *counts.entry(word.len()).or_insert(0) += 1;
    }

    let mut pairs: Vec<(usize, usize)> = counts.into_iter().collect();
    pairs.sort();

    for (len, count) in pairs {
        println!("{}: {}", len, count);
    }
}

输出:

复制代码
1: 1
2: 2
3: 1

解读 :word.len() 返回字节数(对 ASCII 单词等于字符数)。*counts.entry(...).or_insert(0) += 1 是惯用写法,一行完成"查或插,然后自增"。最后排序输出,pairs.sort() 按元组第一个元素排序(长度)。


🔴 练习 10(迭代器链):找最长单词

要求 :用迭代器链找出 "the quick brown fox jumps over the lazy dog" 中最长的单词。
参考答案

rust 复制代码
fn main() {
    let text = "the quick brown fox jumps over the lazy dog";
    let longest = text.split_whitespace()
        .max_by_key(|w| w.len());

    match longest {
        Some(w) => println!("最长: {} ({} 字符)", w, w.len()),
        None => println!("没有单词"),
    }
}

解读 :split_whitespace() 返回迭代器,max_by_key(|w| w.len()) 按长度取最大,返回 Option<&str>。这是迭代器链的典型应用。也可以用 max_by_key 的变体 min_by_key 找最短。


🔵 思考题 1:String 为什么不能用索引访问?

参考答案

因为 String 是 UTF-8 编码,一个字符可能占 1 到 4 个字节。

rust 复制代码
let s = String::from("中文");
// s[0] 会是什么?"中" 占 3 字节,s[0] 只拿到第一个字节的一部分

如果允许 s[0],返回一个字节,但它不构成完整字符,没有意义。Rust 干脆禁止索引,强制你用:

  • s.chars() 按字符遍历
  • s.bytes() 按字节遍历
  • s[0..3] 切片,但必须落在字符边界,否则 panic

解读 :这是 Rust 对"正确性"的坚持。其他语言允许 s[0] 返回字节或字符,容易出 bug。Rust 让你明确选择"按字符"还是"按字节"。字符串处理是很多 bug 的来源,Rust 在这里格外严格。

进阶思考:UTF-8 是变长编码,字符边界不固定。这意味着"索引第 n 个字符"是 O(n) 操作,不是 O(1)。Rust 禁止索引也避免了"看似 O(1) 实际 O(n)"的性能陷阱。


🔵 思考题 2:.get() 和 [] 该用哪个?

参考答案

用 []:确定索引有效,或希望越界时立即崩溃暴露 bug。代码更简洁。

rust 复制代码
let first = &v[0];   // 空 vec 会 panic

用 .get():索引可能越界,需要优雅处理。返回 Option,强制检查。

rust 复制代码
if let Some(x) = v.get(10) {
    println!("{}", x);
} else {
    println!("越界");
}

解读 :和 unwrap vs Result 的选择类似。确定安全用 [],不确定用 .get()。库代码、处理用户输入时优先 .get()。写测试或处理已知不变量时 [] 更简洁。

更深的思考 :[] 的 panic 是一种"断言"------你断言索引有效。如果断言错了,panic 暴露 bug。.get() 则把"可能无效"变成类型的一部分,编译器强制你处理。


🔵 思考题 3:迭代器和 for 循环有什么区别?

参考答案

for 循环本质是迭代器的语法糖:

rust 复制代码
for x in &v { }   // 等价于
let mut iter = (&v).into_iter();
while let Some(x) = iter.next() { }

迭代器链的优势:

组合性强 。map、filter、take、skip、zip 可以自由拼接。

惰性求值 。不调用 collect、sum、for 就不执行,可以构造复杂管道而不浪费中间计算。

性能好。编译器优化后,迭代器链常常和手写循环一样快,甚至更快(避免边界检查)。

声明式 。v.iter().filter(|x| **x > 0).count() 比手写循环更清晰。

解读 :Rust 的迭代器是"零成本抽象"的典范。初学可以先用 for,熟悉后逐步转向迭代器链。两者不是替代关系,for 处理副作用,迭代器链处理数据转换。

什么时候用 for? 循环体有复杂副作用(打印、修改多个变量、错误处理)时,for 更清晰。什么时候用迭代器链?纯数据转换(过滤、映射、聚合)时,链式更简洁。


🔵 思考题 4:Vec::with_capacity 有什么用?

参考答案

预分配容量,避免多次扩容。

rust 复制代码
let mut v = Vec::new();          // 容量 0
for i in 0..1000 {
    v.push(i);                   // 触发多次扩容(4→8→16→...→1024)
}

let mut v = Vec::with_capacity(1000);   // 一次分配够
for i in 0..1000 {
    v.push(i);                   // 不触发扩容
}

扩容的代价:重新分配堆内存、复制旧数据、释放旧内存。1000 次 push 如果不预分配,会触发约 8 次扩容,总复制量约 1000 个元素(均摊 O(1) 但有常数开销)。

什么时候用:

  • 知道大概元素数量 → with_capacity
  • 完全不知道 → Vec::new(),让 Vec 自己管理

解读 :Vec 的扩容策略是"满了翻倍"。这个策略保证 push 的均摊复杂度是 O(1)。但每次扩容都有实际开销,预分配能避免。这在性能敏感场景(比如解析大文件)很重要。

不要过度优化 :初学者不用纠结容量。先让代码跑通,遇到性能瓶颈再优化。with_capacity 是微优化,不是必需。


🔵 思考题 5:HashMap 的键为什么不能是 f64?

参考答案

因为 f64 没有实现 Eq 和 Hash。

HashMap<K, V> 要求 K: Hash + Eq。Eq 要求"自反、对称、传递",特别要求 a == a 永远为真。

f64 的 NaN != NaN,破坏自反性。所以 f64 只实现了 PartialEq,没有实现 Eq。没有 Eq 就不能作为 HashMap 的键。

rust 复制代码
use std::collections::HashMap;

let mut map = HashMap::new();
// map.insert(1.5, "a");   // ❌ f64 不满足 Eq + Hash
map.insert(1, "a");         // ✅ i32 满足

为什么 NaN != NaN 是个问题? 因为 HashMap 依赖"相等"来判断键是否相同。如果 NaN 不等于自己,插入两次 NaN 会被当成两个不同的键,逻辑混乱。

替代方案:

  • 用整数(放大倍数,比如把 1.5 存成 15)
  • 用 ordered_float::OrderedFloat 包装
  • 用 BTreeMap 加自定义比较

解读 :这是"让非法状态无法表示"的又一例。浮点数的 NaN 破坏了很多数学性质,Rust 通过 trait 系统在编译期挡住它。类似的还有 HashMap 的键不能是 Vec<f64>,因为 Vec<f64> 也不满足 Eq。


🔵 思考题 6:迭代器的 map 和 for_each 有什么区别?

参考答案

map 是适配器,返回新迭代器,惰性求值。用于转换元素。

rust 复制代码
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();

for_each 是消费器 ,立即执行,返回 ()。用于副作用。

rust 复制代码
v.iter().for_each(|x| println!("{}", x));

选择:

  • 需要转换后的结果 → map + collect
  • 只需要副作用(打印、写文件)→ for_each
  • 需要链式继续操作 → map
  • 链的终点 → for_each 或 collect

解读 :map 是"转换",for_each 是"消费"。for_each 不能链式(返回 ()),但比 for 循环更简洁。不过 Rust 社区更推荐 for 循环处理副作用------它更易读,也支持 break、continue。

实践建议 :需要 break / continue 用 for;纯副作用且不需要控制流用 for_each;数据转换用 map。


七、知识点总结(第9课)

Vec<T>:动态数组。栈上存指针、长度、容量,堆上存数据。

  • 创建:Vec::new()、vec![]、Vec::with_capacity(n)
  • 增删:push、pop、insert、remove
  • 访问:[] panic、.get() 返回 Option
  • 遍历:&v、&mut v、v

String:可增长 UTF-8 字符串。

  • 创建:String::new()、String::from、.to_string()
  • 拼接:push、push_str、+(左移右借)、format!
  • 不能索引,按 chars() 或 bytes() 遍历
  • len() 返回字节数,不是字符数

HashMap<K, V>:键值对哈希表。

  • 要求 K: Hash + Eq
  • insert、get、entry().or_insert()
  • 遍历顺序不确定
  • 拥有键和值

迭代器:

  • 三种:iter()、iter_mut()、into_iter()
  • 适配器:map、filter、take、skip、chain
  • 消费器:collect、sum、count、max、min、find、any、all
  • 惰性求值,链式组合

易错点:

  • Vec 借用时不能修改(push 可能扩容)
  • String 不能索引,切片要落在字符边界
  • HashMap 键不能是 f64(不满足 Eq)
  • + 拼接字符串会移动左边
  • 迭代器是惰性的,不消费不执行

八、下一课预告

第10课:泛型与 trait

  • 泛型函数与泛型结构体
  • trait 定义与实现
  • trait 作为参数与返回值
  • derive 常用 trait:Debug、Clone、PartialEq
  • 生命周期入门

📌 本课心法:

集合是数据的容器,迭代器是数据的管道。Vec 管顺序,HashMap 管映射,String 管文本。而迭代器把"怎么遍历"和"做什么"解耦------你只描述变换,编译器负责高效执行。从命令式循环到声明式管道,是 Rust 思维的一次跃迁。

集合类型也是所有权规则最常被触发的地方。Vec 的借用与扩容、String 的移动与切片、HashMap 的键值所有权------每一个操作背后,都是第4、5课学的所有权和借用在起作用。理解了集合,才算真正理解了所有权。

相关推荐
龙孚信息1 小时前
跨境电商独立站的安全盲区:一次 PHP 核心漏洞的影响评估
开发语言·安全·php
牛牛牌蜡笔小新1 小时前
Java-常用类库
java·开发语言
sunsijia219831 小时前
2026年10月22日 PHP漏洞临时处理
开发语言·php
Aurorar0rua1 小时前
CS50 x 2024 Notes Python - 02
开发语言·python·学习方法
人人都是软件工程师1 小时前
📚 Tesla AI 总监:让 LLM 把文档"编译"成持续生长的知识库 llm_wiki
人工智能·rust·开源
zyseo81 小时前
谷歌SEO 网站迁移 301 映射脚本:旧站 URL 自动匹配新站结构
开发语言·python
清水白石0082 小时前
React Native与Flutter长期维护成本对比
开发语言·数据库
麦序子2 小时前
用 Python + python-docx 批量生成带表格样式的装修报价单 Word 文档(含合计公式)
开发语言·python·自然语言处理·word·python3.11
1-1=02 小时前
Python PyCharm
开发语言·python·pycharm