欢迎来到我的博客,代码的世界里,每一行都是一个故事


在这里插入图片描述

🎏:你只管努力,剩下的交给时间

🏠 :小破站

前言

上一篇我们把Rust环境搭好了,也跑通了Hello World。但说实话,那还是太简单了。学编程语言,最好的方式就是做点实际的东西出来。

所以这篇文章,我们来写一个真正能用的工具:命令行TODO管理器

为什么选这个项目?因为它:

  • 功能明确:增删改查,逻辑清晰
  • 代码量适中:200行左右,不会看晕
  • 能展示Rust特性:所有权、借用、模式匹配、错误处理…核心概念都能用上
  • 做完能用:真的可以当日常工具用

整个开发过程大概1小时,跟着做完,你对Rust的理解会上一个台阶。


项目需求

先明确一下要做什么功能:

  1. 添加任务todo add "学习Rust"
  2. 列出任务todo list - 显示所有任务
  3. 完成任务todo done 1 - 把ID为1的任务标记为完成
  4. 删除任务todo remove 1 - 删除指定任务
  5. 清空已完成todo clear - 一键清除所有已完成的任务

数据要持久化保存在本地文件(用JSON格式),这样关掉终端再打开,数据还在。


第一步:创建项目

老规矩,用cargo创建项目:

cargo new rust_todo
cd rust_todo
ls -la

创建项目

可以看到cargo帮我们生成了完整的项目结构:

  • .git/ - 已经初始化了git仓库
  • Cargo.toml - 项目配置文件
  • src/main.rs - 源代码入口
  • .gitignore - 已经配置好忽略规则

Cargo真的很贴心,连git都帮你弄好了。


第二步:配置依赖

我们需要三个库:

  1. serde - 序列化和反序列化(把Rust对象转成JSON)
  2. serde_json - JSON处理
  3. chrono - 时间处理(记录任务创建时间)

编辑 Cargo.toml,在 [dependencies] 下添加:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"

配置依赖

为什么需要这些库?

  • serdederive 特性可以自动生成序列化代码,不用手写
  • serde_json 用来读写JSON文件
  • chrono 让时间处理更方便

第三步:编写代码

这是整个项目的核心。完整代码大概200行,我会分模块讲解。

代码整体结构

代码结构

从截图可以看到,代码有212行,主要分为几个部分:

  1. 数据结构定义
  2. 文件操作
  3. 核心功能实现
  4. 命令行参数解析

代码组织得很清晰,这也是Rust的优点之一。

数据结构定义

数据结构

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
    id: u32,
    content: String,
    completed: bool,
    created_at: String,
}

impl Todo {
    fn new(id: u32, content: String) -> Self {
        use chrono::Local;
        let now = Local::now().format("%Y-%m-%d %H:%M").to_string();
        Todo {
            id,
            content,
            completed: false,
            created_at: now,
        }
    }
}

这里有几个Rust特色的东西:

  1. #[derive(...)] - 这是Rust的过程宏,自动实现trait

    • Debug - 可以用 {:?} 打印
    • SerializeDeserialize - 可以转成JSON
    • Clone - 可以复制
  2. impl - 给结构体添加方法

    • fn new() 是构造函数
    • -> Self 表示返回 Todo 类型
    • SelfTodo 的别名
  3. 字段默认不可变 - completed: false 在创建时设置

文件操作

文件操作

fn get_todos_file() -> PathBuf {
    let home = env::var("HOME").expect("无法获取HOME目录");
    PathBuf::from(home).join(".todos.json")
}

fn load_todos() -> Vec<Todo> {
    let file_path = get_todos_file();
    
    if !file_path.exists() {
        return Vec::new();
    }
    
    let data = fs::read_to_string(&file_path).expect("读取文件失败");
    serde_json::from_str(&data).unwrap_or_else(|_| Vec::new())
}

fn save_todos(todos: &Vec<Todo>) {
    let file_path = get_todos_file();
    let json = serde_json::to_string_pretty(todos).expect("序列化失败");
    fs::write(&file_path, json).expect("写入文件失败");
}

这段代码展示了Rust的几个重要概念:

  1. PathBuf - 可变的路径类型

    • .join() 拼接路径
    • 跨平台兼容
  2. 借用 & - save_todos(todos: &Vec<Todo>)

    • & 表示借用,不转移所有权
    • 函数执行完后,调用者还能继续用 todos
  3. 错误处理

    • expect() - 如果出错就panic并显示消息
    • unwrap_or_else() - 出错时提供默认值

为什么 save_todos 要用借用? 因为保存完后,我们可能还要继续操作这个列表,不能把所有权转移走。这就是Rust所有权系统的优雅之处。

核心功能

核心功能

这里实现了 add_todolist_todos 函数。

添加任务的逻辑:

  1. 加载现有任务
  2. 计算新ID(取最大ID + 1)
  3. 创建新任务
  4. 添加到列表
  5. 保存到文件
let new_id = todos.iter().map(|t| t.id).max().unwrap_or(0) + 1;

这行代码很有Rust风格:

  • .iter() - 创建迭代器
  • .map(|t| t.id) - 闭包,提取每个任务的ID
  • .max() - 找最大值,返回 Option<u32>
  • .unwrap_or(0) - 如果没有任务(None),返回0

列出任务的逻辑:

  • 遍历所有任务
  • 已完成的显示 [✓] 和删除线
  • 未完成的显示 [ ]
  • 最后显示统计信息

这里用到了ANSI转义序列

format!("\x1b[9m{}\x1b[0m", todo.content)  // 删除线效果

\x1b[9m 是删除线,\x1b[0m 是重置样式。这让已完成的任务看起来更明显。


第四步:编译项目

代码写完后,第一次编译:

cargo build

编译项目

可以看到cargo在下载和编译依赖:

  • proc-macro2quote - serde需要的宏相关库
  • serdeserde_json - 我们配置的依赖
  • chrono - 时间处理库

第一次编译会慢一点,因为要下载和编译所有依赖。 后续编译就快了,cargo会缓存编译结果。

编译成功后,可执行文件在 target/debug/rust_todo


第五步:测试功能

查看帮助信息

cargo run -- help

帮助信息

注意这里的 --,它告诉cargo:后面的参数是给我们的程序的,不是给cargo的。

帮助信息显示得很清楚:

  • 5个命令的用法
  • 每个命令的说明
  • 使用示例

添加任务

连续添加5个任务:

cargo run -- add "学习Rust所有权系统"
cargo run -- add "写TODO工具"
cargo run -- add "阅读Rust官方文档"
cargo run -- add "练习Rust错误处理"
cargo run -- add "完成第二篇文章"

添加任务

每次添加都显示"✅ 任务已添加!",说明功能正常。

有个细节:后面几次编译都显示 Finished in 0.01s,说明cargo检测到代码没变,直接用缓存了。这就是增量编译的好处。

列出任务

cargo run -- list

任务列表

显示结果很清晰:

  • 每个任务都有ID、内容、创建时间
  • 前面的 [ ] 表示未完成
  • 底部统计:总计5个任务,已完成0个,待办5个

Rust的字符串处理很方便,用 "=".repeat(60) 就能画出分割线。

完成任务

把ID为1和3的任务标记为完成:

cargo run -- done 1
cargo run -- done 3
cargo run -- list

完成任务

再次查看列表,可以看到:

  • ID为1和3的任务前面变成了 [✓]
  • 任务内容有删除线效果(截图可能看不太清楚,但在终端里是有的)
  • 统计更新:已完成2个,待办3个

这里体现了Rust的可变性控制

if let Some(todo) = todos.iter_mut().find(|t| t.id == id) {
    todo.completed = true;
}
  • .iter_mut() - 可变迭代器,可以修改元素
  • if let Some(todo) - 优雅的模式匹配

删除任务

删除ID为4的任务:

cargo run -- remove 4
cargo run -- list

删除任务

删除后,列表从5个任务变成4个。ID为4的"练习Rust错误处理"消失了。

删除用到了 .retain() 方法

todos.retain(|t| t.id != id);

这行代码的意思是:保留所有ID不等于指定ID的任务。简洁又清晰。

清除已完成任务

cargo run -- clear
cargo run -- list

清除已完成任务

执行 clear 命令后,显示"🗑️ 已清除 2 个已完成任务"。

再看列表,只剩2个待办任务了:

  • ID为2的"写TODO工具"
  • ID为5的"完成第二篇文章"

注意ID不连续,这是正常的。因为ID为1和3被清除了,ID为4被删除了。

查看数据文件

cat ~/.todos.json

JSON文件

数据文件是pretty print的JSON格式,可读性很好:

[
  {
    "id": 2,
    "content": "写TODO工具",
    "completed": false,
    "created_at": "2025-11-01 16:17"
  },
  {
    "id": 5,
    "content": "完成第二篇文章",
    "completed": false,
    "created_at": "2025-11-01 16:17"
  }
]

这就是 serde_json::to_string_pretty() 的效果,比压缩的JSON好读多了。

测试错误处理

试试各种错误情况:

cargo run -- done 999    # 不存在的ID
cargo run -- xyz         # 无效命令
cargo run -- add         # 缺少参数

错误处理

每种错误都有友好的提示:

  • 不存在的ID:“❌ 找不到ID为 999 的任务”
  • 无效命令:“❌ 未知命令: xyz” 并显示帮助信息
  • 缺少参数:“❌ 请提供任务内容” 并给出示例

良好的错误提示是优秀工具的标志。 Rust让我们很容易做到这一点。


代码中的Rust特性

通过这个项目,我们实际用到了很多Rust核心特性:

1. 所有权和借用

fn save_todos(todos: &Vec<Todo>)  // 借用,不转移所有权

如果写成 todos: Vec<Todo>(不加&),调用后外面就不能再用这个变量了。加了 & 就只是借用,用完还回去。

2. 模式匹配

match args[1].as_str() {
    "add" => { ... }
    "list" | "ls" => { ... }
    _ => { ... }
}

Rust的 match 必须覆盖所有情况,编译器会检查。这避免了很多bug。

3. Option和Result

todos.iter().map(|t| t.id).max()  // 返回 Option<u32>
fs::read_to_string(&file_path)     // 返回 Result<String, Error>

Rust没有null,用 Option 表示可能没有值。用 Result 表示可能出错。这让错误处理更明确。

4. 迭代器

todos.iter()           // 不可变迭代
todos.iter_mut()       // 可变迭代
todos.iter().filter()  // 过滤
todos.retain()         // 保留符合条件的

Rust的迭代器是零成本抽象,写起来优雅,性能和手写循环一样。

5. 闭包

.map(|t| t.id)                    // 提取字段
.find(|t| t.id == id)             // 查找
.unwrap_or_else(|_| Vec::new())   // 提供默认值

闭包语法简洁,|参数| 是参数列表,后面是函数体。

6. trait和derive

#[derive(Debug, Serialize, Deserialize, Clone)]

通过 derive 自动实现常用trait,不用手写一堆模板代码。


项目总结

这个TODO工具虽然简单,但麻雀虽小五脏俱全:

功能完整

  • ✅ CRUD操作(增删改查)
  • ✅ 数据持久化
  • ✅ 命令行界面
  • ✅ 错误处理
  • ✅ 帮助信息

代码质量

  • ✅ 结构清晰,模块分明
  • ✅ 错误提示友好
  • ✅ 注释合理
  • ✅ 符合Rust习惯

Rust特性应用

  • ✅ 所有权系统保证内存安全
  • ✅ 模式匹配让逻辑清晰
  • ✅ 错误处理显式化
  • ✅ 迭代器优雅高效

大约200行代码,实现了一个真正可用的工具。 这就是Rust的魅力:在保证安全性的同时,代码还能写得很简洁。

彩蛋

获取完整代码:https://minio.acowbo.fun/file/rust_todo.zip

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐