综合项目
综合项目:构建 CLI 任务管理器
Section titled “综合项目:构建 CLI 任务管理器”学习目标: 通过构建一个完整的 Rust CLI 应用来综合本课程的所有内容, 这是一个 Python 开发者通常会用
argparse+json+pathlib编写的内容。难度: 🔴 高级
本综合项目涵盖每个主要章节的概念:
- 第 3 章:类型和变量(结构体、枚举)
- 第 5 章:集合(
Vec、HashMap) - 第 6 章:枚举与模式匹配(任务状态、命令)
- 第 7 章:所有权与借用(传递引用)
- 第 9 章:错误处理(
Result、?、自定义错误) - 第 10 章:Trait(
Display、FromStr) - 第 11 章:类型转换(
From、TryFrom) - 第 12 章:迭代器与闭包(过滤、映射)
- 第 8 章:模块(组织项目结构)
项目:rustdo
Section titled “项目:rustdo”一个命令行任务管理器(类似 Python 的 todo.txt 工具),将任务存储在 JSON 文件中。
Python 等价物(你用 Python 会这样写)
Section titled “Python 等价物(你用 Python 会这样写)”#!/usr/bin/env python3"""简单的 CLI 任务管理器 — Python 版本。"""import jsonimport sysfrom pathlib import Pathfrom datetime import datetimefrom enum import Enum
TASK_FILE = Path.home() / ".rustdo.json"
class Priority(Enum): LOW = "low" MEDIUM = "medium" HIGH = "high"
class Task: def __init__(self, id: int, title: str, priority: Priority, done: bool = False): self.id = id self.title = title self.priority = priority self.done = done self.created = datetime.now().isoformat()
def load_tasks() -> list[Task]: if not TASK_FILE.exists(): return [] data = json.loads(TASK_FILE.read_text()) return [Task(**t) for t in data]
def save_tasks(tasks: list[Task]): TASK_FILE.write_text(json.dumps([t.__dict__ for t in tasks], indent=2))
# 命令:add, list, done, remove, stats# ...(你懂 Python 怎么写)你的 Rust 实现
Section titled “你的 Rust 实现”一步一步构建。每个步骤映射到特定章节的概念。
步骤 1:定义数据模型(第 3、6、10、11 章)
Section titled “步骤 1:定义数据模型(第 3、6、10、11 章)”use std::fmt;use std::str::FromStr;use serde::{Deserialize, Serialize};use chrono::Local;
/// 任务优先级 — 映射到 Python 的 Priority(Enum)#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]#[serde(rename_all = "lowercase")]pub enum Priority { Low, Medium, High,}
// Display trait(Python 的 __str__)impl fmt::Display for Priority { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Priority::Low => write!(f, "low"), Priority::Medium => write!(f, "medium"), Priority::High => write!(f, "high"), } }}
// FromStr trait(解析 "high" → Priority::High)impl FromStr for Priority { type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> { match s.to_lowercase().as_str() { "low" | "l" => Ok(Priority::Low), "medium" | "med" | "m" => Ok(Priority::Medium), "high" | "h" => Ok(Priority::High), other => Err(format!("未知优先级:'{other}'(使用 low/medium/high)")), } }}
/// 单个任务 — 映射到 Python 的 Task 类#[derive(Debug, Clone, Serialize, Deserialize)]pub struct Task { pub id: u32, pub title: String, pub priority: Priority, pub done: bool, pub created: String,}
impl Task { pub fn new(id: u32, title: String, priority: Priority) -> Self { Self { id, title, priority, done: false, created: Local::now().format("%Y-%m-%dT%H:%M:%S").to_string(), } }}
impl fmt::Display for Task { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let status = if self.done { "✅" } else { "⬜" }; let priority_icon = match self.priority { Priority::Low => "🟢", Priority::Medium => "🟡", Priority::High => "🔴", }; write!(f, "{} {} [{}] {} ({})", status, self.id, priority_icon, self.title, self.created) }}Python 对比:在 Python 中你会使用
@dataclass+Enum。在 Rust 中,struct+enum+derive宏免费给你序列化、显示和解析。
步骤 2:存储层(第 9、7 章)
Section titled “步骤 2:存储层(第 9、7 章)”use std::fs;use std::path::PathBuf;use crate::task::Task;
/// 获取任务文件路径(~/.rustdo.json)fn task_file_path() -> PathBuf { let home = dirs::home_dir().expect("无法确定主目录"); home.join(".rustdo.json")}
/// 从磁盘加载任务 — 文件不存在时返回空 Vecpub fn load_tasks() -> Result<Vec<Task>, Box<dyn std::error::Error>> { let path = task_file_path(); if !path.exists() { return Ok(Vec::new()); } let content = fs::read_to_string(&path)?; // ? 传播 io::Error let tasks: Vec<Task> = serde_json::from_str(&content)?; // ? 传播 serde 错误 Ok(tasks)}
/// 保存任务到磁盘pub fn save_tasks(tasks: &[Task]) -> Result<(), Box<dyn std::error::Error>> { let path = task_file_path(); let json = serde_json::to_string_pretty(tasks)?; fs::write(&path, json)?; Ok(())}Python 对比:Python 使用
Path.read_text()+json.loads()。Rust 使用fs::read_to_string()+serde_json::from_str()。注意?— 每个错误都是显式传播的。
步骤 3:命令枚举(第 6 章)
Section titled “步骤 3:命令枚举(第 6 章)”use crate::task::Priority;
/// 所有可能的命令 — 每个动作一个枚举变体pub enum Command { Add { title: String, priority: Priority }, List { show_done: bool }, Done { id: u32 }, Remove { id: u32 }, Stats, Help,}
impl Command { /// 解析命令行参数为 Command /// (生产环境应使用 `clap` — 这里用于教学) pub fn parse(args: &[String]) -> Result<Self, String> { match args.first().map(|s| s.as_str()) { Some("add") => { let title = args.get(1) .ok_or("用法:rustdo add <标题> [优先级]")? .clone(); let priority = args.get(2) .map(|p| p.parse::<Priority>()) .transpose() .map_err(|e| e.to_string())? .unwrap_or(Priority::Medium); Ok(Command::Add { title, priority }) } Some("list") => { let show_done = args.get(1).map(|s| s == "--all").unwrap_or(false); Ok(Command::List { show_done }) } Some("done") => { let id: u32 = args.get(1) .ok_or("用法:rustdo done <id>")? .parse() .map_err(|_| "id 必须是数字")?; Ok(Command::Done { id }) } Some("remove") => { let id: u32 = args.get(1) .ok_or("用法:rustdo remove <id>")? .parse() .map_err(|_| "id 必须是数字")?; Ok(Command::Remove { id }) } Some("stats") => Ok(Command::Stats), _ => Ok(Command::Help), } }}Python 对比:Python 使用
argparse或click。这个手写的解析器展示match如何替代 Python 的 if/elif 链。对于实际项目,使用clapcrate。
步骤 4:业务逻辑(第 5、12、7 章)
Section titled “步骤 4:业务逻辑(第 5、12、7 章)”use crate::task::{Task, Priority};use crate::storage;
pub fn add_task(title: String, priority: Priority) -> Result<(), Box<dyn std::error::Error>> { let mut tasks = storage::load_tasks()?; let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1; let task = Task::new(next_id, title.clone(), priority); println!("已添加:{task}"); tasks.push(task); storage::save_tasks(&tasks)?; Ok(())}
pub fn list_tasks(show_done: bool) -> Result<(), Box<dyn std::error::Error>> { let tasks = storage::load_tasks()?; let filtered: Vec<&Task> = tasks.iter() .filter(|t| show_done || !t.done) // 迭代器 + 闭包(第 12 章) .collect();
if filtered.is_empty() { println!("没有任务!🎉"); return Ok(()); }
for task in &filtered { println!(" {task}"); // 使用 Display trait(第 10 章) } println!("\n显示了 {} 个任务", filtered.len()); Ok(())}
pub fn complete_task(id: u32) -> Result<(), Box<dyn std::error::Error>> { let mut tasks = storage::load_tasks()?; let task = tasks.iter_mut() .find(|t| t.id == id) // Iterator::find(第 12 章) .ok_or(format!("没有 id 为 {id} 的任务"))?; task.done = true; println!("已完成:{task}"); storage::save_tasks(&tasks)?; Ok(())}
pub fn remove_task(id: u32) -> Result<(), Box<dyn std::error::Error>> { let mut tasks = storage::load_tasks()?; let len_before = tasks.len(); tasks.retain(|t| t.id != id); // Vec::retain(第 5 章) if tasks.len() == len_before { return Err(format!("没有 id 为 {id} 的任务").into()); } println!("已删除任务 {id}"); storage::save_tasks(&tasks)?; Ok(())}
pub fn show_stats() -> Result<(), Box<dyn std::error::Error>> { let tasks = storage::load_tasks()?; let total = tasks.len(); let done = tasks.iter().filter(|t| t.done).count(); let pending = total - done;
// 使用迭代器按优先级分组(第 12 章) let high = tasks.iter().filter(|t| !t.done && t.priority == Priority::High).count(); let medium = tasks.iter().filter(|t| !t.done && t.priority == Priority::Medium).count(); let low = tasks.iter().filter(|t| !t.done && t.priority == Priority::Low).count();
println!("📊 任务统计"); println!(" 总数: {total}"); println!(" 已完成: {done} ✅"); println!(" 待办: {pending}"); println!(" 🔴 高: {high}"); println!(" 🟡 中: {medium}"); println!(" 🟢 低: {low}"); Ok(())}使用的关键 Rust 模式:
iter().map().max()、iter().filter().collect()、iter_mut().find()、retain()、iter().filter().count()。这些替代了 Python 的列表推导式、next(x for x in ...)和Counter。
步骤 5:连接一切(第 8 章)
Section titled “步骤 5:连接一切(第 8 章)”mod task;mod storage;mod command;mod actions;
use command::Command;
fn main() { let args: Vec<String> = std::env::args().skip(1).collect(); let command = match Command::parse(&args) { Ok(cmd) => cmd, Err(e) => { eprintln!("错误:{e}"); std::process::exit(1); } };
let result = match command { Command::Add { title, priority } => actions::add_task(title, priority), Command::List { show_done } => actions::list_tasks(show_done), Command::Done { id } => actions::complete_task(id), Command::Remove { id } => actions::remove_task(id), Command::Stats => actions::show_stats(), Command::Help => { print_help(); Ok(()) } };
if let Err(e) = result { eprintln!("错误:{e}"); std::process::exit(1); }}
fn print_help() { println!("rustdo — Python 开发者学习 Rust 的任务管理器\n"); println!("用法:"); println!(" rustdo add <标题> [low|medium|high] 添加任务"); println!(" rustdo list [--all] 列出待办任务"); println!(" rustdo done <id> 标记任务完成"); println!(" rustdo remove <id> 删除任务"); println!(" rustdo stats 显示统计");}graph TD CLI["main.rs<br/>(CLI 入口)"] --> CMD["command.rs<br/>(解析参数)"] CMD --> ACT["actions.rs<br/>(业务逻辑)"] ACT --> STORE["storage.rs<br/>(JSON 持久化)"] ACT --> TASK["task.rs<br/>(数据模型)"] STORE --> TASK style CLI fill:#d4edda style CMD fill:#fff3cd style ACT fill:#fff3cd style STORE fill:#ffeeba style TASK fill:#ffeeba步骤 6:Cargo.toml 依赖
Section titled “步骤 6:Cargo.toml 依赖”[package]name = "rustdo"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"chrono = "0.4"dirs = "5"Python 等价物:这是你的
pyproject.toml[project.dependencies]。cargo add serde serde_json chrono dirs类似于pip install。
步骤 7:测试(第 14 章)
Section titled “步骤 7:测试(第 14 章)”// src/task.rs — 添加到底部#[cfg(test)]mod tests { use super::*;
#[test] fn parse_priority() { assert_eq!("high".parse::<Priority>().unwrap(), Priority::High); assert_eq!("H".parse::<Priority>().unwrap(), Priority::High); assert_eq!("med".parse::<Priority>().unwrap(), Priority::Medium); assert!("invalid".parse::<Priority>().is_err()); }
#[test] fn task_display() { let task = Task::new(1, "写 Rust".to_string(), Priority::High); let display = format!("{task}"); assert!(display.contains("写 Rust")); assert!(display.contains("🔴")); assert!(display.contains("⬜")); // 尚未完成 }
#[test] fn task_serialization_roundtrip() { let task = Task::new(1, "测试".to_string(), Priority::Low); let json = serde_json::to_string(&task).unwrap(); let recovered: Task = serde_json::from_str(&json).unwrap(); assert_eq!(recovered.title, "测试"); assert_eq!(recovered.priority, Priority::Low); }}Python 等价物:
pytest测试。用cargo test运行而非pytest。无需测试发现魔法 —#[test]显式标记测试函数。
基础版本工作后,尝试这些增强:
-
添加
clap进行参数解析 — 用clap的 derive 宏替换手写解析器:#[derive(Parser)]enum Command {Add { title: String, #[arg(default_value = "medium")] priority: Priority },List { #[arg(long)] all: bool },Done { id: u32 },Remove { id: u32 },Stats,} -
添加彩色输出 — 使用
coloredcrate 实现终端颜色(类似 Python 的colorama)。 -
添加截止日期 — 添加
Option<NaiveDate>字段并过滤过期任务。 -
添加标签/分类 — 使用
Vec<String>存储标签并用.iter().any()过滤。 -
使其成为库 + 二进制 — 分离为
lib.rs+main.rs以便逻辑可重用(第 8 章模块模式)。
你练习了什么
Section titled “你练习了什么”| 章节 | 概念 | 出现位置 |
|---|---|---|
| 第 3 章 | 类型和变量 | Task 结构体字段、u32、String、bool |
| 第 5 章 | 集合 | Vec<Task>、retain()、push() |
| 第 6 章 | 枚举 + match | Priority、Command、穷举匹配 |
| 第 7 章 | 所有权 + 借用 | &[Task] vs Vec<Task>、&mut 用于完成 |
| 第 8 章 | 模块 | mod task; mod storage; mod command; mod actions; |
| 第 9 章 | 错误处理 | Result<T, E>、? 运算符、.ok_or() |
| 第 10 章 | Trait | Display、FromStr、Serialize、Deserialize |
| 第 11 章 | From/Into | FromStr 用于 Priority、.into() 用于错误转换 |
| 第 12 章 | 迭代器 | filter、map、find、count、collect |
| 第 14 章 | 测试 | #[test]、#[cfg(test)]、断言宏 |
🎓 恭喜! 如果你构建了这个项目,你已经使用了本书涵盖的所有主要 Rust 概念。你不再是”学习 Rust 的 Python 开发者” — 你是一个也懂 Python 的 Rust 开发者。