概述

本教程将通过一个完整的实战项目,综合运用前面学到的 Rust 知识。我们将构建一个简单的命令行工具——任务管理器(Todo 应用)。

项目规划

功能需求

  1. 添加任务

  2. 列出所有任务

  3. 标记任务完成

  4. 删除任务

  5. 将任务保存到文件

  6. 从文件加载任务

项目结构

todo_app/
├── Cargo.toml
├── src/
│   ├── main.rs
│   ├── task.rs
│   ├── storage.rs
│   └── cli.rs
└── tasks.json

创建项目

初始化项目

cargo new todo_app
cd todo_app

配置 Cargo.toml

[package]
name = "todo_app"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
clap = { version = "3.0", features = ["derive"] }
colored = "2.0"
dirs = "4.0"

实现核心模块

task.rs - 任务模型

use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub description: String,
    pub completed: bool,
}

impl Task {
    pub fn new(id: u32, description: String) -> Self {
        Task {
            id,
            description,
            completed: false,
        }
    }
    
    pub fn mark_completed(&mut self) {
        self.completed = true;
    }
}

impl fmt::Display for Task {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let status = if self.completed {
            "[✓]"
        } else {
            "[ ]"
        };
        write!(f, "{} {}: {}", status, self.id, self.description)
    }
}

storage.rs - 存储管理

use crate::task::Task;
use serde_json;
use std::fs;
use std::path::PathBuf;

pub struct Storage {
    file_path: PathBuf,
}

impl Storage {
    pub fn new(file_path: PathBuf) -> Self {
        Storage { file_path }
    }
    
    pub fn save_tasks(&self, tasks: &[Task]) -> Result<(), String> {
        let json = serde_json::to_string_pretty(tasks)
            .map_err(|e| format!("序列化失败: {}", e))?;
        
        fs::write(&self.file_path, json)
            .map_err(|e| format!("写入文件失败: {}", e))?;
        
        Ok(())
    }
    
    pub fn load_tasks(&self) -> Result<Vec<Task>, String> {
        if !self.file_path.exists() {
            return Ok(Vec::new());
        }
        
        let content = fs::read_to_string(&self.file_path)
            .map_err(|e| format!("读取文件失败: {}", e))?;
        
        let tasks: Vec<Task> = serde_json::from_str(&content)
            .map_err(|e| format!("反序列化失败: {}", e))?;
        
        Ok(tasks)
    }
}

cli.rs - 命令行接口

use clap::{Parser, Subcommand};
use colored::*;

#[derive(Parser)]
#[clap(name = "todo")]
#[clap(about = "一个简单的任务管理器", long_about = None)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// 添加新任务
    Add {
        /// 任务描述
        description: String,
    },
    /// 列出所有任务
    List,
    /// 标记任务为完成
    Complete {
        /// 任务 ID
        id: u32,
    },
    /// 删除任务
    Delete {
        /// 任务 ID
        id: u32,
    },
}

pub fn parse_args() -> Commands {
    let cli = Cli::parse();
    cli.command
}

pub fn print_success(message: &str) {
    println!("{}", message.green());
}

pub fn print_error(message: &str) {
    eprintln!("{}", message.red());
}

pub fn print_info(message: &str) {
    println!("{}", message.blue());
}

main.rs - 主程序

mod task;
mod storage;
mod cli;

use crate::task::Task;
use crate::storage::Storage;
use crate::cli::{parse_args, Commands, print_success, print_error, print_info};
use dirs::home_dir;
use std::path::PathBuf;

struct TodoApp {
    tasks: Vec<Task>,
    storage: Storage,
}

impl TodoApp {
    fn new() -> Result<Self, String> {
        let mut data_dir = home_dir().ok_or("无法找到主目录")?;
        data_dir.push(".todo");
        
        std::fs::create_dir_all(&data_dir)
            .map_err(|e| format!("创建目录失败: {}", e))?;
        
        let file_path = data_dir.join("tasks.json");
        let storage = Storage::new(file_path);
        let tasks = storage.load_tasks()?;
        
        Ok(TodoApp { tasks, storage })
    }
    
    fn add_task(&mut self, description: String) -> Result<(), String> {
        let id = self.tasks.len() as u32 + 1;
        let task = Task::new(id, description);
        self.tasks.push(task);
        self.save()?;
        print_success(&format!("任务已添加: {}", description));
        Ok(())
    }
    
    fn list_tasks(&self) {
        if self.tasks.is_empty() {
            print_info("没有任务");
            return;
        }
        
        println!("\n任务列表:");
        println!("{}", "─".repeat(40));
        for task in &self.tasks {
            println!("{}", task);
        }
        println!("{}", "─".repeat(40));
        println!("总计: {} 个任务", self.tasks.len());
    }
    
    fn complete_task(&mut self, id: u32) -> Result<(), String> {
        let task = self.tasks
            .iter_mut()
            .find(|t| t.id == id)
            .ok_or_else(|| format!("任务 ID {} 不存在", id))?;
        
        if task.completed {
            print_info(&format!("任务 {} 已经完成", id));
        } else {
            task.mark_completed();
            self.save()?;
            print_success(&format!("任务 {} 标记为完成", id));
        }
        
        Ok(())
    }
    
    fn delete_task(&mut self, id: u32) -> Result<(), String> {
        let index = self.tasks
            .iter()
            .position(|t| t.id == id)
            .ok_or_else(|| format!("任务 ID {} 不存在", id))?;
        
        self.tasks.remove(index);
        self.save()?;
        print_success(&format!("任务 {} 已删除", id));
        Ok(())
    }
    
    fn save(&self) -> Result<(), String> {
        self.storage.save_tasks(&self.tasks)
    }
    
    fn run(&mut self) -> Result<(), String> {
        let command = parse_args();
        
        match command {
            Commands::Add { description } => {
                self.add_task(description)?;
            }
            Commands::List => {
                self.list_tasks();
            }
            Commands::Complete { id } => {
                self.complete_task(id)?;
            }
            Commands::Delete { id } => {
                self.delete_task(id)?;
            }
        }
        
        Ok(())
    }
}

fn main() {
    match TodoApp::new() {
        Ok(mut app) => {
            if let Err(e) = app.run() {
                print_error(&e);
                std::process::exit(1);
            }
        }
        Err(e) => {
            print_error(&format!("初始化失败: {}", e));
            std::process::exit(1);
        }
    }
}

测试项目

运行项目

cargo run -- add "学习 Rust"
cargo run -- add "编写代码"
cargo run -- list
cargo run -- complete 1
cargo run -- list
cargo run -- delete 2
cargo run -- list

查看帮助

cargo run -- --help

添加测试

task.rs 测试

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_new_task() {
        let task = Task::new(1, "测试任务".to_string());
        assert_eq!(task.id, 1);
        assert_eq!(task.description, "测试任务");
        assert_eq!(task.completed, false);
    }
    
    #[test]
    fn test_mark_completed() {
        let mut task = Task::new(1, "测试任务".to_string());
        task.mark_completed();
        assert_eq!(task.completed, true);
    }
}

storage.rs 测试

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::NamedTempFile;
    
    #[test]
    fn test_save_and_load() {
        let temp_file = NamedTempFile::new().unwrap();
        let storage = Storage::new(temp_file.path().to_path_buf());
        
        let tasks = vec![
            Task::new(1, "任务1".to_string()),
            Task::new(2, "任务2".to_string()),
        ];
        
        storage.save_tasks(&tasks).unwrap();
        let loaded_tasks = storage.load_tasks().unwrap();
        
        assert_eq!(loaded_tasks.len(), 2);
        assert_eq!(loaded_tasks[0].description, "任务1");
        assert_eq!(loaded_tasks[1].description, "任务2");
    }
}

运行测试

cargo test

添加文档

模块文档

//! 任务管理器
 //!
//! 提供任务的创建、修改和显示功能。

use serde::{Deserialize, Serialize};
use std::fmt;

/// 表示一个任务
/// 
/// # Fields
 /// 
/// * `id` - 任务的唯一标识符
 /// * `description` - 任务描述
 /// * `completed` - 任务是否完成
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    // ...
}

生成文档

cargo doc --open

优化和改进

添加搜索功能

impl TodoApp {
    fn search_tasks(&self, keyword: &str) {
        let matching_tasks: Vec<&Task> = self.tasks
            .iter()
            .filter(|t| t.description.contains(keyword))
            .collect();
        
        if matching_tasks.is_empty() {
            print_info(&format!("没有找到包含 '{}' 的任务", keyword));
        } else {
            println!("\n匹配的任务:");
            println!("{}", "─".repeat(40));
            for task in matching_tasks {
                println!("{}", task);
            }
        }
    }
}

添加优先级

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub description: String,
    pub completed: bool,
    pub priority: Priority,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Priority {
    Low,
    Medium,
    High,
}

添加截止日期

use chrono::{DateTime, Utc};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub description: String,
    pub completed: bool,
    pub due_date: Option<DateTime<Utc>>,
}

构建和发布

构建发布版本

cargo build --release

安装到系统

cargo install --path .

发布到 crates.io

cargo login
cargo publish

总结

本教程通过一个完整的任务管理器项目,展示了如何使用 Rust 构建实际应用:

  1. 项目规划

    • 定义功能需求
    • 设计项目结构
    • 配置依赖
  2. 模块实现

    • Task 模型:定义任务结构和行为
    • Storage 模块:处理数据持久化
    • CLI 模块:处理命令行参数
    • 主程序:协调各个模块
  3. 测试

    • 单元测试
    • 集成测试
    • 运行测试
  4. 文档

    • 添加注释
    • 生成文档
  5. 优化改进

    • 搜索功能
    • 优先级
    • 截止日期
  6. 构建发布

    • 构建发布版本
    • 安装到系统
    • 发布到 crates.io

通过这个项目,你学会了如何:

  • 使用 Cargo 管理项目

  • 组织代码模块

  • 处理错误

  • 使用外部库

  • 编写测试

  • 编写文档

  • 构建和发布

这个项目可以作为基础,继续扩展更多功能,如任务分类、提醒、同步等。

下一步

在最后一篇教程中,我们将学习 Rust 的最佳实践,包括:

  • 代码风格

  • 性能优化

  • 安全性

  • 可维护性

  • 常见陷阱和解决方案

继续学习 Rust,掌握这门强大语言的更多特性!