Rust 集合类型全解:Vector/HashMap/ 字符串,新手必学的 3 个核心结构
=# Rust集合类型全解:Vector/HashMap/字符串,新手必学的3个核心结构
Rust的标准库集合类型,是日常开发中覆盖90%业务场景的核心工具——它们全部存储在堆内存中,无需在编译期确定长度,支持动态增删改查,完美解决了固定长度数组的局限性。
其中,动态数组Vec、字符串String/&str、哈希表HashMap<K,V> 是最基础、最常用的三个核心结构。很多新手写Rust代码踩坑,80%都来自对这三个类型的内存布局、所有权规则、API边界理解不到位。本文将从底层原理到常用API,再到新手必踩坑,最后通过完整实战带你吃透这三个集合类型,写出安全高效的Rust代码。
一、动态数组 Vec:有序序列的首选
Vec<T>是Rust的动态数组,和其他语言的数组/列表类似,用于存储相同类型、有序、可重复的元素,支持O(1)时间的索引访问和尾部增删,是Rust中最基础的集合类型。
1.1 核心内存布局
理解Vec的内存布局,是避免新手坑的关键。Vec本质上是一个包含三个字段的胖指针,存储在栈上,实际的元素数据存储在堆上:
// 简化的Vec底层结构
struct Vec<T> {
ptr: *mut T, // 裸指针,指向堆上存储元素的连续内存块
len: usize, // 当前已存储的元素个数
capacity: usize, // 堆内存块总共能容纳的元素个数(预分配的容量)
}
- 当
len == capacity时,再执行push操作会触发扩容:Rust会申请一块更大的内存(默认扩容策略:容量小于1024时翻倍,大于等于1024时增长50%),将原有元素拷贝到新内存,释放旧内存,同时更新ptr和capacity。 - 扩容是新手坑的核心来源之一:扩容后原有堆内存地址改变,之前获取的元素引用会直接失效,编译器会强制报错。
1.2 高频常用方法(带实战示例)
1. 创建Vec
fn main() {
// 1. 创建空Vec(推荐,初始容量为0,首次push自动扩容)
let mut v1: Vec<i32> = Vec::new();
// 2. 带预分配容量的创建(高频场景必用,避免多次扩容,提升性能)
let mut v2: Vec<String> = Vec::with_capacity(100); // 预分配100个元素的空间
// 3. 用vec!宏快速创建带初始值的Vec(最常用)
let v3 = vec![1, 2, 3, 4, 5]; // 自动推断类型为Vec<i32>
let v4 = vec![0; 10]; // 创建10个元素,每个都是0,等价于vec![0,0,...0]
}
2. 增:添加元素
fn main() {
let mut v = Vec::new();
// 1. 尾部添加单个元素(O(1) amortized)
v.push(1);
v.push(2);
v.push(3); // v = [1,2,3]
// 2. 指定位置插入元素(O(n),后面的元素会向后移动,少用!)
v.insert(1, 10); // v = [1,10,2,3]
// 3. 尾部批量添加元素(extend,比循环push高效)
v.extend([4,5,6]); // v = [1,10,2,3,4,5,6]
v.extend(vec![7,8]); // 支持任何实现了Iterator的类型
}
3. 删:删除元素
fn main() {
let mut v = vec![1,2,3,4,5,6];
// 1. 尾部弹出元素(O(1),返回Option<T>,空Vec返回None)
let last = v.pop(); // last = Some(6), v = [1,2,3,4,5]
// 2. 删除指定索引的元素(O(n),后面的元素向前移动,少用!)
let removed = v.remove(1); // removed = 2, v = [1,3,4,5]
// 3. 批量删除:保留符合条件的元素(retain,高效过滤)
v.retain(|x| x % 2 == 1); // 只保留奇数,v = [1,3,5]
// 4. 清空所有元素(保留容量)
v.clear(); // v = [], capacity不变
}
4. 改:修改元素
fn main() {
let mut v = vec![1,2,3,4,5];
// 1. 索引直接修改(注意:索引越界会直接panic!)
v[0] = 10; // v = [10,2,3,4,5]
// 2. 遍历修改所有元素(iter_mut,可变借用,安全)
for num in v.iter_mut() {
*num *= 2; // 每个元素乘2,v = [20,4,6,8,10]
}
// 3. 安全修改指定位置元素(get_mut,返回Option,无panic风险)
if let Some(num) = v.get_mut(2) {
*num = 100; // 索引2存在,修改为100,v = [20,4,100,8,10]
}
}
5. 查:访问元素&遍历
fn main() {
let v = vec![1,2,3,4,5];
// 1. 安全索引访问(get,返回Option<&T>,越界返回None,推荐)
let first = v.get(0); // Some(&1)
let out_of_bounds = v.get(10); // None,不会panic
// 2. 直接索引访问([],越界直接panic,生产环境慎用!)
let second = v[1]; // 2
// 3. 判断元素是否存在
let has_three = v.contains(&3); // true
// 4. 不可变遍历(iter,只读,不转移所有权,最常用)
for num in v.iter() {
println!("{}", num);
}
// 5. 消费型遍历(into_iter,转移所有权,遍历后原Vec不可用)
for num in v.into_iter() {
println!("{}", num);
}
// 这里v已经被转移所有权,无法再使用!
}
1.3 新手必踩的5个坑&避坑指南
| 坑点 | 反面示例 | 正确做法 |
|---|---|---|
| 索引越界panic | let x = v[100]; 数组长度不足直接崩溃 |
用v.get(100),通过if let/ match处理None情况 |
| 扩容导致引用失效 | let first = &v[0]; v.push(1); println!("{}", first); |
不要在持有元素引用的同时执行push/insert等可能触发扩容的操作 |
| into_iter转移所有权后复用原Vec | for x in v.into_iter() {}; v.push(1); 编译报错 |
只读遍历用v.iter(),修改用v.iter_mut(),只有需要消费Vec时才用into_iter |
| 频繁用remove删除头部元素 | for _ in 0..1000 { v.remove(0); } 性能极差,每次都要移动所有元素 |
队列场景用std::collections::VecDeque,批量删除用retain,无需保持顺序用swap_remove |
| 忽略预分配导致频繁扩容 | 循环10万次push,初始用Vec::new() |
提前知道元素数量时,用Vec::with_capacity(n)预分配内存,减少扩容次数 |
二、字符串 String/&str:新手最容易踩坑的类型
Rust的字符串是新手入门的最大拦路虎之一,它和Python/Java等语言的字符串设计完全不同,核心根源是:Rust的字符串严格遵循UTF-8编码,且在编译期强制保证内存安全。
2.1 核心概念与内存布局
Rust有两个核心字符串类型,必须彻底分清:
- &str:字符串切片,是对一段UTF-8编码字节序列的不可变借用,本质是胖指针(包含指向字节序列的指针+长度),没有所有权。
- 字符串字面量
"hello world"就是&'static str类型,存储在程序的静态只读内存中,生命周期贯穿整个程序运行周期。
- 字符串字面量
- String:拥有所有权的、可增长的、可变的UTF-8字符串,本质是对
Vec<u8>的封装,内存布局和Vec完全一致(栈上的胖指针+堆上的UTF-8字节数据)。
简单总结:String是你拥有的房子,可以随便装修改造;&str是房子的门牌号,只能用来访问,不能修改房子本身。
2.2 高频常用方法(带实战示例)
1. String的创建
fn main() {
// 1. 创建空String
let mut s1 = String::new();
// 2. 从字符串字面量创建(两种常用方式)
let s2 = String::from("hello rust");
let s3 = "hello rust".to_string();
// 3. 格式化创建(最常用的拼接方式,无所有权转移)
let name = "张三";
let age = 20;
let s4 = format!("姓名:{},年龄:{}", name, age); // 姓名:张三,年龄:20
}
2. String的修改(可变操作)
fn main() {
let mut s = String::from("hello");
// 1. 尾部追加字符串切片
s.push_str(" world"); // s = "hello world"
// 2. 尾部追加单个字符
s.push('!'); // s = "hello world!"
// 3. 插入字符串/字符(指定位置,注意:按字节索引,非字符!)
s.insert(5, ", rust"); // s = "hello, rust world!"
// 4. 替换内容
s = s.replace("world", "Rust"); // s = "hello, rust Rust!"
// 5. 弹出尾部字符
let last = s.pop(); // last = Some('!')
// 6. 清空字符串
s.clear(); // s = ""
}
3. 字符串切片&str的常用方法
fn main() {
let s = " Hello Rust 你好 ";
// 1. 获取字节长度(注意:不是字符数!UTF-8中中文占3个字节)
let byte_len = s.len(); // 20,空格+英文+中文的总字节数
// 2. 获取Unicode字符数(正确的"字符长度")
let char_count = s.chars().count(); // 14,正确的字符个数
// 3. 去除首尾空白
let trimmed = s.trim(); // "Hello Rust 你好"
// 4. 分割字符串
let parts: Vec<&str> = trimmed.split_whitespace().collect(); // ["Hello", "Rust", "你好"]
// 5. 判断前缀/后缀/包含
let starts_with_hello = trimmed.starts_with("Hello"); // true
let ends_with_hi = trimmed.ends_with("你好"); // true
let contains_rust = trimmed.contains("Rust"); // true
// 6. 遍历Unicode字符(唯一正确的遍历每个字符的方式)
for c in trimmed.chars() {
println!("{}", c);
}
// 7. 字符串转其他类型(parse,返回Result)
let num_str = "123";
let num: i32 = num_str.parse().unwrap(); // 123
}
4. 类型转换
fn main() {
let s = String::from("hello");
// 1. String 转 &str(自动解引用,直接用&即可,最常用)
let s_slice: &str = &s;
// 2. &str 转 String
let s1: String = "hello".to_string();
let s2: String = String::from("hello");
// 3. String 转 字节数组Vec<u8>(消费所有权)
let bytes: Vec<u8> = s.into_bytes();
// 4. 字节数组 转 String(验证UTF-8合法性)
let valid_bytes = vec![104, 101, 108, 108, 111];
let s_from_bytes = String::from_utf8(valid_bytes).unwrap(); // "hello"
// 处理非法UTF-8字节,用from_utf8_lossy,非法字符替换为�
let invalid_bytes = vec![104, 101, 0xFF, 108, 111];
let s_lossy = String::from_utf8_lossy(&invalid_bytes); // "he�lo"
}
2.3 新手必踩的6个坑&避坑指南
| 坑点 | 反面示例 | 正确做法 |
|---|---|---|
| 用len()获取中文字符长度,结果错误 | "你好".len() == 2 实际返回6,因为每个中文占3个字节 |
获取字符数用.chars().count(),字节长度用.len() |
| 用[]索引访问中文字符,编译报错/panic | "你好"[0] 访问的是第一个字节0xE4,不是’你’,索引1会直接panic |
访问第n个字符用.chars().nth(n),返回Option,安全无panic |
| 字符串拼接转移所有权,原变量无法使用 | let s3 = s1 + &s2; println!("{}", s1); 编译报错,s1所有权被转移 |
多字符串拼接优先用format!()宏,不会转移任何变量的所有权 |
| 函数参数用String而非&str,导致不必要的拷贝 | fn print(s: String) { println!("{}", s); } 调用时必须传入拥有所有权的String |
只读场景函数参数一律用&str,支持String和字符串字面量传入,无所有权转移 |
| 随意修改字节破坏UTF-8编码,导致乱码 | let mut s = String::from("你好"); unsafe { s.as_mut_vec()[0] = 0xFF; } 直接乱码 |
不要直接修改字符串的底层字节,如需修改,通过chars()处理字符后重新构建String |
| 混淆字符和字节的索引,导致split/insert出错 | s.insert(2, 'x') 插入到"你好"的第2个字节,破坏UTF-8编码 |
所有涉及位置的操作,先通过chars()遍历确定字符的字节边界,再执行操作 |
三、哈希表 HashMap<K,V>:键值对快速查找的首选
HashMap<K,V>是基于哈希表实现的键值对集合,用于存储键值对映射,要求键必须唯一,且实现了Eq和Hash trait,支持O(1)平均时间的增删改查,是处理映射关系、缓存、字典场景的核心结构。
3.1 核心原理
Rust标准库的HashMap默认使用SipHash哈希函数,能有效防止哈希碰撞攻击,同时保证不错的性能。核心特性:
- 键必须唯一,插入重复键时,旧值会被覆盖;
- 无序存储,遍历顺序和插入顺序无关(如需有序,可用
BTreeMap); - 有容量和负载因子(默认0.9),当键值对数量超过
容量*负载因子时,会触发扩容,重新哈希所有键值对。
3.2 高频常用方法(带实战示例)
1. 创建HashMap
use std::collections::HashMap;
fn main() {
// 1. 创建空HashMap
let mut map1: HashMap<String, i32> = HashMap::new();
// 2. 带预分配容量的创建(提前知道数量时必用,避免多次扩容和重新哈希)
let mut map2: HashMap<u32, String> = HashMap::with_capacity(100);
// 3. 从迭代器创建(比如Vec转HashMap)
let vec = vec![("a", 1), ("b", 2), ("c", 3)];
let map3: HashMap<_, _> = vec.into_iter().collect();
}
2. 增:插入键值对
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
// 1. 基础插入(键存在则覆盖旧值,返回旧值Option<V>)
let old_value = scores.insert("张三".to_string(), 90); // old_value = None
let old_value2 = scores.insert("张三".to_string(), 95); // old_value2 = Some(90),旧值被覆盖
// 2. 高级插入:entry API(Rust HashMap的灵魂,仅一次哈希查找,高效无panic)
// 键不存在则插入默认值,返回值的可变引用
scores.entry("李四".to_string()).or_insert(80); // 李四不存在,插入80
scores.entry("张三".to_string()).or_insert(0); // 张三已存在,不修改
// 经典场景:统计单词出现次数
let words = vec!["apple", "banana", "apple", "orange", "banana", "apple"];
let mut count_map = HashMap::new();
for word in words {
*count_map.entry(word).or_insert(0) += 1;
}
// count_map = {"apple":3, "banana":2, "orange":1}
}
3. 删:删除键值对
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::from([
("张三", 95),
("李四", 80),
("王五", 88),
]);
// 1. 删除指定键,返回对应的值Option<V>
let removed = scores.remove("李四"); // removed = Some(80)
// 2. 批量删除:保留符合条件的键值对
scores.retain(|_, score| *score >= 90); // 只保留90分以上的,只剩"张三"
// 3. 清空所有键值对
scores.clear();
}
4. 改:修改值
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::from([
("张三", 95),
("李四", 80),
]);
// 1. 覆盖修改(insert)
scores.insert("李四", 85); // 李四的分数改为85
// 2. 安全修改:get_mut(返回Option<&mut V>,键不存在返回None)
if let Some(score) = scores.get_mut("张三") {
*score += 5; // 张三的分数加5,变为100
}
// 3. 不存在则插入,存在则修改(entry API,最优雅)
scores.entry("王五".to_string()).and_modify(|score| *score += 2).or_insert(88);
}
5. 查:访问值&遍历
use std::collections::HashMap;
fn main() {
let scores = HashMap::from([
("张三", 95),
("李四", 80),
("王五", 88),
]);
// 1. 安全访问:get(返回Option<&V>,键不存在返回None,推荐)
let zhang_score = scores.get("张三"); // Some(&95)
let zhao_score = scores.get("赵六"); // None,无panic
// 2. 直接索引访问([],键不存在直接panic,生产环境慎用!)
let li_score = scores["李四"]; // 80
// 3. 判断键是否存在
let has_wang = scores.contains_key("王五"); // true
// 4. 不可变遍历(iter,只读,不转移所有权)
for (name, score) in scores.iter() {
println!("{}: {}", name, score);
}
// 5. 可变遍历(iter_mut,可修改值)
let mut scores_mut = scores.clone();
for (_, score) in scores_mut.iter_mut() {
*score += 2; // 所有人分数加2
}
}
3.3 新手必踩的5个坑&避坑指南
| 坑点 | 反面示例 | 正确做法 |
|---|---|---|
| 键不存在用[]索引,直接panic | let score = scores["赵六"]; 键不存在直接崩溃 |
用scores.get("赵六"),通过if let/match处理None情况 |
| 先contains_key再insert,两次哈希查找,性能差 | if !map.contains_key(&k) { map.insert(k, v); } |
用map.entry(k).or_insert(v),仅一次哈希查找,代码更简洁 |
| 用f64/f32作为键,出现无法查找的问题 | 用f64当键,NaN不等于自身,插入后无法找到 |
绝对不要用浮点数作为HashMap的键,改用整数、字符串等实现了稳定Eq的类型 |
| 插入后复用原变量,所有权被转移 | let name = "张三".to_string(); map.insert(name, 90); println!("{}", name); 编译报错 |
插入时如果不需要复用原变量,直接传入字面量;需要复用则传入引用(注意生命周期),或clone() |
| 遍历过程中修改HashMap,导致逻辑错误/编译报错 | 遍历的时候执行remove/insert操作,触发借用规则报错 | 遍历的时候先收集需要修改/删除的键,遍历结束后再批量操作,避免遍历过程中修改HashMap |
四、实战:学生成绩管理小工具(全集合类型落地)
我们通过一个完整的命令行学生成绩管理工具,把上面三个核心集合类型全部落地,同时用上所有安全API,避开新手坑。
4.1 需求说明
工具实现6个核心功能,覆盖所有集合类型的常用场景:
- 添加学生:录入学号(唯一)、姓名、各科成绩;
- 查询学生:通过学号查询学生的各科成绩、平均分、排名;
- 修改成绩:通过学号修改学生的指定科目成绩;
- 删除学生:通过学号删除学生信息;
- 班级统计:统计全班平均分、各科最高分/最低分、分数段分布;
- 列出所有学生:按平均分从高到低排序,输出所有学生信息。
4.2 数据结构设计(全集合类型应用)
use std::collections::HashMap;
/// 学生结构体
#[derive(Debug, Clone)]
struct Student {
id: u32, // 学号,唯一标识
name: String, // 姓名,String类型(拥有所有权)
scores: HashMap<String, f32>, // 各科成绩,HashMap:键=科目名,值=分数
}
/// 班级成绩管理器
struct ScoreManager {
// 核心存储:HashMap,键=学号,值=学生信息,O(1)时间增删改查
students: HashMap<u32, Student>,
}
4.3 完整实现代码
use std::collections::HashMap;
use std::io;
#[derive(Debug, Clone)]
struct Student {
id: u32,
name: String,
scores: HashMap<String, f32>,
}
impl Student {
/// 计算学生的平均分
fn average(&self) -> f32 {
if self.scores.is_empty() {
return 0.0;
}
let sum: f32 = self.scores.values().sum();
sum / self.scores.len() as f32
}
}
struct ScoreManager {
students: HashMap<u32, Student>,
}
impl ScoreManager {
/// 创建新的成绩管理器
fn new() -> Self {
Self {
students: HashMap::new(),
}
}
/// 1. 添加学生,返回是否成功(学号重复则失败)
fn add_student(&mut self, student: Student) -> Result<(), String> {
// entry API:学号不存在则插入,存在则返回错误
if self.students.contains_key(&student.id) {
return Err(format!("学号{}已存在", student.id));
}
self.students.insert(student.id, student);
Ok(())
}
/// 2. 通过学号查询学生,返回Option(安全无panic)
fn get_student(&self, id: u32) -> Option<&Student> {
self.students.get(&id)
}
/// 3. 修改学生的指定科目成绩
fn update_score(&mut self, id: u32, subject: &str, new_score: f32) -> Result<(), String> {
// 安全获取可变引用,学号不存在返回错误
let student = self.students.get_mut(&id)
.ok_or(format!("学号{}不存在", id))?;
// 修改成绩,科目不存在则自动添加
student.scores.insert(subject.to_string(), new_score);
Ok(())
}
/// 4. 删除学生
fn delete_student(&mut self, id: u32) -> Result<(), String> {
// remove返回Option,判断是否存在
self.students.remove(&id)
.ok_or(format!("学号{}不存在", id))?;
Ok(())
}
/// 5. 班级统计
fn class_statistics(&self) {
if self.students.is_empty() {
println!("暂无学生数据");
return;
}
// 用Vec存储所有学生的平均分,用于计算班级平均分
let all_averages: Vec<f32> = self.students
.values()
.map(|s| s.average())
.collect();
let class_average: f32 = all_averages.iter().sum::<f32>() / all_averages.len() as f32;
let max_average = all_averages.iter().fold(f32::MIN, |a, &b| a.max(b));
let min_average = all_averages.iter().fold(f32::MAX, |a, &b| a.min(b));
// 用HashMap统计各科的最高分、最低分
let mut subject_stats: HashMap<String, (f32, f32, f32)> = HashMap::new();
for student in self.students.values() {
for (subject, score) in &student.scores {
let entry = subject_stats.entry(subject.clone()).or_insert((f32::MAX, f32::MIN, 0.0));
entry.0 = entry.0.min(*score); // 最低分
entry.1 = entry.1.max(*score); // 最高分
entry.2 += *score; // 总分
}
}
// 输出统计结果
println!("===== 班级统计 =====");
println!("学生总数:{}人", self.students.len());
println!("班级平均分:{:.2}", class_average);
println!("班级最高平均分:{:.2}", max_average);
println!("班级最低平均分:{:.2}", min_average);
println!("\n各科成绩统计:");
for (subject, (min, max, sum)) in subject_stats {
let avg = sum / self.students.len() as f32;
println!("{}:最低分{:.2},最高分{:.2},平均分{:.2}", subject, min, max, avg);
}
}
/// 6. 按平均分从高到低列出所有学生
fn list_all_students(&self) {
if self.students.is_empty() {
println!("暂无学生数据");
return;
}
// 把HashMap的学生引用转为Vec,用于排序(HashMap无序)
let mut sorted_students: Vec<&Student> = self.students.values().collect();
// 按平均分降序排序
sorted_students.sort_by(|a, b| b.average().partial_cmp(&a.average()).unwrap());
println!("===== 学生排名 =====");
for (rank, student) in sorted_students.iter().enumerate() {
println!("第{}名:学号{},姓名{},平均分{:.2}",
rank + 1, student.id, student.name, student.average());
println!(" 各科成绩:{:?}", student.scores);
}
}
/// 计算学生的班级排名
fn get_student_rank(&self, id: u32) -> Option<usize> {
let mut sorted_students: Vec<&Student> = self.students.values().collect();
sorted_students.sort_by(|a, b| b.average().partial_cmp(&a.average()).unwrap());
sorted_students.iter().position(|s| s.id == id).map(|p| p + 1)
}
}
/// 辅助函数:读取用户输入
fn read_input() -> String {
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取输入失败");
input.trim().to_string()
}
fn main() {
let mut manager = ScoreManager::new();
println!("===== 学生成绩管理系统 =====");
loop {
println!("\n请选择功能:");
println!("1. 添加学生");
println!("2. 查询学生");
println!("3. 修改成绩");
println!("4. 删除学生");
println!("5. 班级统计");
println!("6. 列出所有学生");
println!("0. 退出系统");
print!("请输入选项:");
let choice = read_input();
match choice.as_str() {
"1" => {
// 添加学生
println!("请输入学号:");
let id: u32 = match read_input().parse() {
Ok(n) => n,
Err(_) => {
println!("学号必须是数字!");
continue;
}
};
println!("请输入姓名:");
let name = read_input();
let mut scores = HashMap::new();
println!("请输入科目数量:");
let subject_count: usize = match read_input().parse() {
Ok(n) => n,
Err(_) => {
println!("科目数量必须是数字!");
continue;
}
};
for i in 0..subject_count {
println!("请输入第{}个科目名称:", i + 1);
let subject = read_input();
println!("请输入{}的分数:", subject);
let score: f32 = match read_input().parse() {
Ok(s) => s,
Err(_) => {
println!("分数必须是数字!");
continue;
}
};
scores.insert(subject, score);
}
let student = Student { id, name, scores };
match manager.add_student(student) {
Ok(_) => println!("添加成功!"),
Err(e) => println!("添加失败:{}", e),
}
}
"2" => {
// 查询学生
println!("请输入要查询的学号:");
let id: u32 = match read_input().parse() {
Ok(n) => n,
Err(_) => {
println!("学号必须是数字!");
continue;
}
};
match manager.get_student(id) {
Some(student) => {
println!("\n===== 学生信息 =====");
println!("学号:{}", student.id);
println!("姓名:{}", student.name);
println!("各科成绩:{:?}", student.scores);
println!("平均分:{:.2}", student.average());
if let Some(rank) = manager.get_student_rank(id) {
println!("班级排名:第{}名", rank);
}
}
None => println!("学号{}不存在", id),
}
}
"3" => {
// 修改成绩
println!("请输入学生学号:");
let id: u32 = match read_input().parse() {
Ok(n) => n,
Err(_) => {
println!("学号必须是数字!");
continue;
}
};
println!("请输入要修改的科目名称:");
let subject = read_input();
println!("请输入新的分数:");
let new_score: f32 = match read_input().parse() {
Ok(s) => s,
Err(_) => {
println!("分数必须是数字!");
continue;
}
};
match manager.update_score(id, &subject, new_score) {
Ok(_) => println!("修改成功!"),
Err(e) => println!("修改失败:{}", e),
}
}
"4" => {
// 删除学生
println!("请输入要删除的学号:");
let id: u32 = match read_input().parse() {
Ok(n) => n,
Err(_) => {
println!("学号必须是数字!");
continue;
}
};
match manager.delete_student(id) {
Ok(_) => println!("删除成功!"),
Err(e) => println!("删除失败:{}", e),
}
}
"5" => {
// 班级统计
manager.class_statistics();
}
"6" => {
// 列出所有学生
manager.list_all_students();
}
"0" => {
println!("退出系统,再见!");
break;
}
_ => {
println!("无效选项,请重新输入!");
}
}
}
}
4.4 代码核心亮点
- 全集合类型覆盖:
HashMap<u32, Student>:核心存储,实现学号的快速增删改查;HashMap<String, f32>:学生各科成绩,科目名到分数的映射;Vec<&Student>:用于排序和统计,实现排名功能;String/&str:姓名、科目、用户输入的处理,全程安全无乱码。
- 零panic风险:所有可能出错的场景都用
Result/Option处理,没有无保障的unwrap()和索引访问; - 高效API使用:全程用
entry API、get/get_mut等高效安全的方法,避免多次哈希查找和不必要的拷贝; - 所有权规范:只读场景用借用,需要修改用可变借用,避免不必要的所有权转移和内存拷贝。
五、总结:三个集合的使用场景选择
| 集合类型 | 核心优势 | 适用场景 |
|---|---|---|
| Vec | 有序、O(1)索引访问、尾部增删高效 | 列表、栈、队列、需要有序存储的同类型元素 |
| String/&str | 严格UTF-8安全、高效的字符串处理 | 所有文本处理场景,String用于拥有和修改,&str用于只读借用 |
| HashMap<K,V> | O(1)平均时间的键值对查找 | 唯一键映射、缓存、字典、需要快速通过键查找值的场景 |
对于Rust新手来说,吃透这三个集合类型,就掌握了Rust日常开发的核心数据结构。记住一个核心原则:永远优先使用安全的API(get/entry/iter等),避免会导致panic的直接索引和unwrap(),同时时刻关注所有权和借用规则,就能避开90%的坑,写出安全高效的Rust代码。
更多推荐




所有评论(0)