2. 结构体与集合篇¶
对应章节:第5-6章。
本篇掌握结构体封装、方法、Vec 与 HashMap 集合,以及携带数据的枚举与递归。
项目 4:待办事项管理器¶
目标:用结构体封装数据和方法,实现交互式待办列表。
创建项目¶
功能要求¶
add:添加待办remove:删除指定编号list:显示全部quit:退出
知识点¶
| 知识点 | 说明 |
|---|---|
struct |
定义结构体 |
impl 方法 |
关联函数 |
&mut self |
可变方法(修改数据) |
&self |
只读方法 |
Vec<String> |
动态数组 |
.enumerate() |
带索引迭代 |
stdin + match |
交互循环 |
splitn(2, ' ') |
最多切 2 段 |
代码¶
src/main.rs:
use std::io;
// 结构体:封装"待办列表"这个数据
struct TodoList {
items: Vec<String>, // Vec:动态扩容的数组
}
// impl 块:为结构体定义方法
impl TodoList {
// 关联函数(无 self):构造新实例,约定用 new 命名
fn new() -> TodoList {
TodoList { items: Vec::new() }
}
// &mut self:修改自身数据需要可变借用
fn add(&mut self, task: String) {
self.items.push(task);
println!("已添加");
}
// &mut self:删除也要修改
fn remove(&mut self, index: usize) {
// index 是 1-based,转 0-based 需减 1
// 先做边界检查,否则 remove 越界会 panic
if index >= 1 && index <= self.items.len() {
self.items.remove(index - 1);
println!("已删除");
} else {
println!("编号无效");
}
}
// &self:只读,不修改数据
fn list(&self) {
if self.items.is_empty() {
println!("(暂无待办)");
}
// enumerate() 产生 (索引, 元素) 对,从 0 开始
for (i, item) in self.items.iter().enumerate() {
println!("{}. {}", i + 1, item); // 显示时 +1 变成 1-based
}
}
}
fn main() {
let mut todos = TodoList::new();
loop {
println!("\n请输入命令:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取失败");
let input = input.trim(); // trim 去除换行符
// splitn(2, ' '):最多切成 2 段,内容里的空格不受影响
// 例如 "add 买 牛奶" → ["add", "买 牛奶"]
let parts: Vec<&str> = input.splitn(2, ' ').collect();
match parts[0] {
"add" => {
if parts.len() > 1 {
todos.add(parts[1].to_string());
}
}
"remove" => {
if parts.len() > 1 {
if let Ok(idx) = parts[1].parse::<usize>() {
todos.remove(idx);
}
}
}
"list" => todos.list(),
"quit" => break, // 退出循环结束程序
_ => println!("未知命令"),
}
}
}
运行¶
注意事项¶
splitn(2, ' '):只把输入切成「命令 + 剩余内容」两部分,内容里带空格也不受影响。&mut selfvs&self:add/remove修改数据用&mut self;list只读用&self。remove边界检查:items.remove(i)越界会 panic,先检查index >= 1 && index <= len。
项目 5:联系人通讯录¶
目标:用 HashMap + 结构体存储联系人,支持增删查。
创建项目¶
功能要求¶
知识点¶
| 知识点 | 说明 |
|---|---|
HashMap |
键值对集合 |
struct Contact |
联系人结构 |
Option |
get 返回 Option |
遍历 values() |
读取全部值 |
name.clone() |
避免所有权冲突 |
.is_some() |
Option 判存在 |
代码¶
src/main.rs:
use std::collections::HashMap;
use std::io;
// 单个联系人
struct Contact {
name: String,
phone: String,
}
// 通讯录:用 HashMap,键 = 姓名,值 = Contact
struct AddressBook {
contacts: HashMap<String, Contact>,
}
impl AddressBook {
fn new() -> AddressBook {
AddressBook { contacts: HashMap::new() }
}
fn add(&mut self, name: String, phone: String) {
// 关键:name 既要做 key 又要放进 Contact
// 直接 insert(name, Contact{name, ...}) 会导致 name 被移动两次 → 编译错误
// 所以 key 用 name.clone()
self.contacts.insert(name.clone(), Contact { name, phone });
println!("已添加");
}
// get 返回 Option<&Contact>:存在返回 Some(借用),不存在返回 None
fn find(&self, name: &str) -> Option<&Contact> {
self.contacts.get(name)
}
// remove 返回被删的值(Option),is_some() 判断是否真的删了
fn remove(&mut self, name: &str) -> bool {
self.contacts.remove(name).is_some()
}
// values() 迭代所有值
fn list(&self) {
for c in self.contacts.values() {
println!("{}: {}", c.name, c.phone);
}
}
}
fn main() {
let mut book = AddressBook::new();
loop {
println!("\n请输入命令:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("读取失败");
// 这里用 split_whitespace(按任意空白切,姓名电话各占一段)
let parts: Vec<&str> = input.trim().split_whitespace().collect();
match parts[0] {
"add" => {
if parts.len() >= 3 {
book.add(parts[1].to_string(), parts[2].to_string());
}
}
"find" => {
if parts.len() >= 2 {
// 用 match 处理 Option,避免索引越界
match book.find(parts[1]) {
Some(c) => println!("{}: {}", c.name, c.phone),
None => println!("未找到:{}", parts[1]),
}
}
}
"remove" => {
if parts.len() >= 2 {
if book.remove(parts[1]) {
println!("已删除");
} else {
println!("未找到");
}
}
}
"list" => book.list(),
"quit" => break,
_ => println!("未知命令"),
}
}
}
运行¶
注意事项¶
insert(name.clone(), Contact{name, ...}):key 和 value 都要用 name,必须先 clone,否则 name 被移进结构体后 key 用不了。get返回Option<&Contact>:用match处理存在/不存在,避免索引越界。remove(...).is_some():HashMap 的 remove 返回被删的值(Option),用is_some()判断是否真的删掉了。
项目 6:表达式求值器¶
目标:支持 + - * /、括号、优先级和一元负号的表达式求值,核心是"携带数据的枚举 + 递归下降解析"。
创建项目¶
💡
calculator——项目2的进阶版,与项目2同名。
功能要求¶
- 支持
+ - * /与小数 - 支持括号嵌套改变优先级
- 支持一元负号(如
-3) - 先打印词法切分结果,再输出计算值
知识点¶
| 知识点 | 说明 |
|---|---|
Number(f64) 变体存储数值 |
携带数据的枚举 |
#[derive(Debug, Clone, Copy)] |
自动派生常用 trait |
match 分发 |
按 Token 类型分支处理 |
| 递归函数 | 括号内是完整表达式 → 递归求解 |
&mut usize |
多个函数共享"当前解析位置" |
peekable() / peek() |
预读下一个字符但不消费 |
'0'..='9' 模式 |
字符范围匹配 |
| 递归下降解析 | 三层函数对应三层优先级 |
代码¶
src/main.rs:
use std::env;
// 词法单元(Token):表达式被切分后的最小组成部分
// Number 携带数据(数值本身),其余变体表示运算符/括号
// derive 自动实现:Debug(打印)、Clone/Copy(按值复制,匹配时不移动)
#[derive(Debug, Clone, Copy)]
enum Token {
Number(f64),
Plus,
Minus,
Star,
Slash,
LParen,
RParen,
}
// 第一步:词法分析——把字符串切成 Token 序列
// "1 + 2*3" → [Number(1.0), Plus, Number(2.0), Star, Number(3.0)]
fn tokenize(s: &str) -> Vec<Token> {
let mut tokens = Vec::new();
// peekable:给迭代器装上"预读"能力,peek() 看下一个字符但不消费
let mut iter = s.chars().peekable();
while let Some(&c) = iter.peek() {
match c {
' ' => {
iter.next(); // 跳过空格
}
'+' => {
tokens.push(Token::Plus);
iter.next();
}
'-' => {
tokens.push(Token::Minus);
iter.next();
}
'*' => {
tokens.push(Token::Star);
iter.next();
}
'/' => {
tokens.push(Token::Slash);
iter.next();
}
'(' => {
tokens.push(Token::LParen);
iter.next();
}
')' => {
tokens.push(Token::RParen);
iter.next();
}
// 数字或小数点:连续读取组成完整数字(如 12.5)
'0'..='9' | '.' => {
let mut num = String::new();
while let Some(&c) = iter.peek() {
if c.is_ascii_digit() || c == '.' {
num.push(c);
iter.next();
} else {
break;
}
}
tokens.push(Token::Number(num.parse().unwrap()));
}
_ => panic!("无法识别的字符:{}", c),
}
}
tokens
}
// 第二步:语法分析(递归下降)——三层函数对应三层优先级
// parse_expr 处理 + -(最低优先级,最后结合)
fn parse_expr(tokens: &[Token], pos: &mut usize) -> f64 {
let mut value = parse_term(tokens, pos);
while *pos < tokens.len() {
match tokens[*pos] {
Token::Plus => {
*pos += 1;
value += parse_term(tokens, pos);
}
Token::Minus => {
*pos += 1;
value -= parse_term(tokens, pos);
}
_ => break, // 不是 +/-,交回上层处理
}
}
value
}
// parse_term 处理 * /(中优先级,比 +/- 结合更紧)
fn parse_term(tokens: &[Token], pos: &mut usize) -> f64 {
let mut value = parse_factor(tokens, pos);
while *pos < tokens.len() {
match tokens[*pos] {
Token::Star => {
*pos += 1;
value *= parse_factor(tokens, pos);
}
Token::Slash => {
*pos += 1;
let divisor = parse_factor(tokens, pos);
if divisor == 0.0 {
panic!("除数不能为 0");
}
value /= divisor;
}
_ => break,
}
}
value
}
// parse_factor 处理数字 / 括号 / 一元负号(最高优先级)
fn parse_factor(tokens: &[Token], pos: &mut usize) -> f64 {
match tokens[*pos] {
Token::Number(n) => {
*pos += 1;
n
}
Token::LParen => {
*pos += 1; // 吃掉 '('
// 括号里是一个完整表达式 → 递归调用 parse_expr
let value = parse_expr(tokens, pos);
match tokens.get(*pos) {
Some(Token::RParen) => {
*pos += 1; // 吃掉 ')'
value
}
_ => panic!("语法错误:预期右括号"),
}
}
Token::Minus => {
// 一元负号:如 -3、-(1+2),取负后面的因子
*pos += 1;
let value = parse_factor(tokens, pos);
-value
}
_ => panic!("语法错误:预期数字"),
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let expr = &args[1];
// 先词法分析,打印切分结果便于观察
let tokens = tokenize(expr);
println!("{:?}", tokens);
// 再语法分析求值;pos 是所有 parse_* 函数共享的"游标"
let mut pos = 0;
let result = parse_expr(&tokens, &mut pos);
println!("{}", result);
}
运行¶
cargo run -- "1 + 2 * 3"
# [Number(1.0), Plus, Number(2.0), Star, Number(3.0)]
# 7
cargo run -- "(1 + 2) * 3" # 9
cargo run -- "-3 + 5" # 2
cargo run -- "10 / (2 + 3)" # 2
cargo run -- "1 / 0" # panic:除数不能为 0
⚠️ 表达式必须加引号:否则 shell 会把
*展开成文件名、拆散括号。
注意事项¶
- 三层函数 = 三层优先级:
parse_expr(+ -)调用parse_term(* /),后者再调用parse_factor(数字/括号/负号)。低优先级层在高优先级层之上,乘法自然比加法先结合。 &mut usize共享游标:所有parse_*函数操作同一个位置索引,*pos += 1消费 token——这是多个函数协作修改同一状态的经典写法。Copy让匹配不移动:match tokens[*pos]按值取出 Token;若没有#[derive(Copy)],Number(f64)里的 f64 虽可 Copy,但枚举含非 Copy 字段时就会报移动错误。加上 derive 后整个 Token 可按位复制。- 括号 → 递归:
parse_factor遇到(就递归调用parse_expr——嵌套表达式天然适合递归结构。 - 错误处理是 panic:非法输入直接崩溃。学完第9章后,可回头把这些
panic!改造成Result<f64, ParseError>——很好的复习练习。 - 改进练习:不给参数直接运行会因
args[1]越界而 panic。参考项目2的做法,自己加上参数检查与用法提示。