首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Rust实战:200行代码写一个命令行TODO工具

Rust实战:200行代码写一个命令行TODO工具

原创
作者头像
一只牛博
发布2026-01-03 14:46:51
发布2026-01-03 14:46:51
1.3K0
举报
文章被收录于专栏:rustrust

前言

上一篇我们把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创建项目:

代码语言:bash
复制
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] 下添加:

代码语言:toml
复制
[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的优点之一。

数据结构定义

数据结构
数据结构
代码语言: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 在创建时设置

文件操作

文件操作
文件操作
代码语言:rust
复制
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. 保存到文件
代码语言:rust
复制
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转义序列

代码语言:rust
复制
format!("\x1b[9m{}\x1b[0m", todo.content)  // 删除线效果

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


第四步:编译项目

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

代码语言:bash
复制
cargo build
编译项目
编译项目

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

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

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

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


第五步:测试功能

查看帮助信息

代码语言:bash
复制
cargo run -- help
帮助信息
帮助信息

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

帮助信息显示得很清楚:

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

添加任务

连续添加5个任务:

代码语言:bash
复制
cargo run -- add "学习Rust所有权系统"
cargo run -- add "写TODO工具"
cargo run -- add "阅读Rust官方文档"
cargo run -- add "练习Rust错误处理"
cargo run -- add "完成第二篇文章"
添加任务
添加任务

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

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

列出任务

代码语言:bash
复制
cargo run -- list
任务列表
任务列表

显示结果很清晰:

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

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

完成任务

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

代码语言:bash
复制
cargo run -- done 1
cargo run -- done 3
cargo run -- list
完成任务
完成任务

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

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

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

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

删除任务

删除ID为4的任务:

代码语言:bash
复制
cargo run -- remove 4
cargo run -- list
删除任务
删除任务

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

删除用到了 .retain() 方法

代码语言:rust
复制
todos.retain(|t| t.id != id);

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

清除已完成任务

代码语言:bash
复制
cargo run -- clear
cargo run -- list
清除已完成任务
清除已完成任务

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

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

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

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

查看数据文件

代码语言:bash
复制
cat ~/.todos.json
JSON文件
JSON文件

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

代码语言: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好读多了。

测试错误处理

试试各种错误情况:

代码语言:bash
复制
cargo run -- done 999    # 不存在的ID
cargo run -- xyz         # 无效命令
cargo run -- add         # 缺少参数
错误处理
错误处理

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

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

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


代码中的Rust特性

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

1. 所有权和借用

代码语言:rust
复制
fn save_todos(todos: &Vec<Todo>)  // 借用,不转移所有权

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

2. 模式匹配

代码语言:rust
复制
match args[1].as_str() {
    "add" => { ... }
    "list" | "ls" => { ... }
    _ => { ... }
}

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

3. Option和Result

代码语言:rust
复制
todos.iter().map(|t| t.id).max()  // 返回 Option<u32>
fs::read_to_string(&file_path)     // 返回 Result<String, Error>

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

4. 迭代器

代码语言:rust
复制
todos.iter()           // 不可变迭代
todos.iter_mut()       // 可变迭代
todos.iter().filter()  // 过滤
todos.retain()         // 保留符合条件的

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

5. 闭包

代码语言:rust
复制
.map(|t| t.id)                    // 提取字段
.find(|t| t.id == id)             // 查找
.unwrap_or_else(|_| Vec::new())   // 提供默认值

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

6. trait和derive

代码语言:rust
复制
#[derive(Debug, Serialize, Deserialize, Clone)]

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


项目总结

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

功能完整

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

代码质量

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

Rust特性应用

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 项目需求
  • 第一步:创建项目
  • 第二步:配置依赖
  • 第三步:编写代码
    • 代码整体结构
    • 数据结构定义
    • 文件操作
    • 核心功能
  • 第四步:编译项目
  • 第五步:测试功能
    • 查看帮助信息
    • 添加任务
    • 列出任务
    • 完成任务
    • 删除任务
    • 清除已完成任务
    • 查看数据文件
    • 测试错误处理
  • 代码中的Rust特性
    • 1. 所有权和借用
    • 2. 模式匹配
    • 3. Option和Result
    • 4. 迭代器
    • 5. 闭包
    • 6. trait和derive
  • 项目总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档