第四章:模块化设计与错误处理

第四章:模块化设计与错误处理

教学目标

  • 掌握 Rust 模块系统与 Crate 管理的使用方法
  • 理解 Rust 错误处理机制与最佳实践
  • 掌握文件操作与 JSON 序列化 / 反序列化的实现
  • 实现任务数据的文件持久化功能

核心知识点

1. 模块与 Crate

模块声明(mod)与文件拆分

Rust 的模块系统用于组织代码结构,提高代码的可维护性和复用性。使用mod关键字声明模块,通常一个模块对应一个独立的文件。

rust 复制代码
// src/lib.rs 或 src/main.rs
mod module1;  // 声明module1模块,对应module1.rs文件
mod module2;  // 声明module2模块,对应module2.rs文件
fn main() {
    // 使用模块中的函数
    module1::function1();
    module2::function2();
}
// src/module1.rs
pub fn function1() {
    println!("这是module1中的function1");
}
// src/module2.rs
pub fn function2() {
    println!("这是module2中的function2");
}
use 语句与路径解析

use语句用于导入模块中的项目,避免重复书写完整路径。可以导入模块、结构体、函数等。

rust 复制代码
// 导入标准库中的Vec
use std::vec::Vec;
// 导入标准库中的HashMap,使用as关键字重命名
use std::collections::HashMap as Hashmap;
// 导入自定义模块中的Task结构体
use crate::task::Task;
fn main() {
    let vec: Vec<i32> = vec![1, 2, 3];
    let mut map: Hashmap<String, i32> = Hashmap::new();
    let task = Task::new(1, "任务".to_string(), "描述".to_string(), SystemTime::now());
}
Cargo.toml 依赖管理

Cargo 通过Cargo.toml文件管理项目依赖,添加依赖时需要在[dependencies]部分声明。

rust 复制代码
# Cargo.toml
[package]
name = "rusttask"
version = "0.1.0"
edition = "2021"
[dependencies]
# 声明serde依赖,用于数据序列化/反序列化
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

2. 错误处理

Result 类型与?操作符

Result<T, E>枚举用于表示可能成功或失败的操作,?操作符可以简化错误处理代码,自动传播错误。

rust 复制代码
use std::fs::File;
use std::io::Read;
fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;  // 打开文件,失败时返回错误
    let mut content = String::new();
    file.read_to_string(&mut content)?;  // 读取文件内容,失败时返回错误
    Ok(content)  // 成功时返回内容
}
fn main() {
    match read_file("data.txt") {
        Ok(content) => println!("文件内容: {}", content),
        Err(e) => println!("读取文件错误: {}", e),
    }
}
自定义错误类型

使用thiserror库可以方便地定义自定义错误类型,需要在Cargo.toml中添加依赖。

rust 复制代码
# Cargo.toml
[package]
name = "rusttask"
version = "0.1.0"
edition = "2021"
[ dependencies ]
thiserror = "1.0"
rust 复制代码
use thiserror::Error;
// 定义自定义错误类型
#[derive(Error, Debug)]
pub enum AppError {
    #[error("文件操作错误: {0}")]
    FileError(#[from] std::io::Error),
    
    #[error("JSON解析错误: {0}")]
    JsonError(#[from] serde_json::Error),
    
    #[error("任务不存在: ID为{0}的任务不存在")]
    TaskNotFound(u32),
}
fn process_data() -> Result<(), AppError> {
    // 读取文件,自动转换为AppError
    let content = std::fs::read_to_string("data.json")?;
    // 解析JSON,自动转换为AppError
    let data: Vec<i32> = serde_json::from_str(&content)?;
    
    println!("解析数据: {:?}", data);
    Ok(())
}
fn main() {
    if let Err(e) = process_data() {
        println!("处理数据错误: {}", e);
    }
}
错误传播与处理策略

错误处理策略包括:直接返回错误、记录错误并继续执行、转换错误类型等。

rust 复制代码
use std::fs::File;
use std::io::{Read, Write};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
    #[error("文件操作错误: {0}")]
    FileError(#[from] std::io::Error),
    
    #[error("数据格式错误: {0}")]
    FormatError(String),
}
// 策略1:直接返回错误
fn read_data1(path: &str) -> Result<String, AppError> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}
// 策略2:记录错误并返回默认值
fn read_data2(path: &str) -> Result<String, AppError> {
    match File::open(path) {
        Ok(mut file) => {
            let mut content = String::new();
            if let Err(e) = file.read_to_string(&mut content) {
                eprintln!("读取文件错误: {}", e);
                return Ok("默认内容".to_string());
            }
            Ok(content)
        },
        Err(e) => {
            eprintln!("打开文件错误: {}", e);
            Ok("默认内容".to_string())
        }
    }
}
// 策略3:转换错误类型
fn read_data3(path: &str) -> Result<String, AppError> {
    let content = std::fs::read_to_string(path).map_err(|e| {
        AppError::FileError(e)
    })?;
    Ok(content)
}

3. 文件操作与 JSON 序列化

std::fs 模块文件读写

std::fs模块提供了文件和目录操作的函数,包括创建、读取、写入文件等。

rust 复制代码
use std::fs;
use std::io::Write;
fn main() {
    // 写入文件
    let content = "这是要写入文件的内容";
    fs::write("data.txt", content).expect("写入文件失败");
    
    // 读取文件
    let content = fs::read_to_string("data.txt").expect("读取文件失败");
    println!("文件内容: {}", content);
    
    // 追加内容到文件
    let mut file = fs::OpenOptions::new()
        .write(true)
        .append(true)
        .open("data.txt")
        .expect("打开文件失败");
    writeln!(file, "这是追加的内容").expect("追加内容失败");
}
serde_json 库实现数据序列化 / 反序列化

serde_json库用于将 Rust 数据结构转换为 JSON 格式(序列化)和将 JSON 格式转换为 Rust 数据结构(反序列化),需要配合serde库的derive特性。

rust 复制代码
use serde::{Deserialize, Serialize};
use serde_json;
// 定义可序列化/反序列化的结构体
#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    is_student: bool,
}
fn main() {
    // 序列化:Rust结构体 -> JSON
    let person = Person {
        name: "张三".to_string(),
        age: 20,
        is_student: true,
    };
    
    // 序列化为JSON字符串
    let json_str = serde_json::to_string(&person).unwrap();
    println!("JSON字符串: {}", json_str);
    
    // 写入JSON到文件
    serde_json::to_writer(std::fs::File::create("person.json").unwrap(), &person).unwrap();
    
    // 反序列化:JSON -> Rust结构体
    let json_data = r#"{"name":"李四","age":22,"is_student":false}"#;
    let person: Person = serde_json::from_str(json_data).unwrap();
    println!("反序列化结果: {:?}", person);
    
    // 从文件读取JSON并反序列化
    let person_from_file: Person = serde_json::from_reader(std::fs::File::open("person.json").unwrap()).unwrap();
    println!("从文件反序列化结果: {:?}", person_from_file);
}
数据模型与 JSON 格式的映射

Rust 数据结构与 JSON 格式的映射规则:

  • 结构体 -> JSON 对象
  • 枚举 -> JSON 字符串(默认使用变体名称)
  • 字段 -> JSON 属性
  • 向量 / 数组 -> JSON 数组
  • 基本类型 -> 对应 JSON 类型
rust 复制代码
use serde::{Deserialize, Serialize};
use serde_json;
// 定义带枚举的结构体
#[derive(Serialize, Deserialize, Debug)]
enum Gender {
    Male,
    Female,
}
#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u8,
    gender: Gender,
    hobbies: Vec<String>,
}
fn main() {
    let person = Person {
        name: "张三".to_string(),
        age: 20,
        gender: Gender::Male,
        hobbies: vec!["阅读".to_string(), "编程".to_string()],
    };
    
    // 序列化为JSON
    let json_str = serde_json::to_string_pretty(&person).unwrap();
    println!("JSON:");
    println!("{}", json_str);
    
    // 输出:
    // {
    //   "name": "张三",
    //   "age": 20,
    //   "gender": "Male",
    //   "hobbies": [
    //     "阅读",
    //     "编程"
    //   ]
    // }
}

项目实战:实现任务数据持久化

1. 拆分模块

将项目拆分为task、cli和storage三个模块,分别负责任务数据模型、命令行交互和数据存储。

css 复制代码
src/
├── main.rs
├── task.rs
├── cli.rs
└── storage.rs

2. 定义存储相关错误类型

storage.rs中定义自定义错误类型StorageError,用于处理存储操作中的错误。

rust 复制代码
// src/storage.rs
use thiserror::Error;
use std::io::Error as IoError;
use serde_json::Error as JsonError;
// 存储模块的自定义错误类型
#[derive(Error, Debug)]
pub enum StorageError {
    #[error("文件操作错误: {0}")]
    FileError(#[from] IoError),
    
    #[error("JSON解析错误: {0}")]
    JsonError(#[from] JsonError),
    
    #[error("任务数据格式错误: {0}")]
    DataFormatError(String),
}

3. 实现文件存储功能

storage.rs中实现基于 JSON 文件的任务数据存储功能,包括保存和加载任务数据。

rust 复制代码
// src/storage.rs
use crate::task::{Task, TaskStatus};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use super::StorageError;
// 序列化任务数据的结构体
#[derive(Serialize, Deserialize, Debug)]
struct TaskStorage {
    tasks: HashMap<u32, Task>,
    next_id: u32,
}
// 文件存储实现
pub struct FileStorage {
    path: PathBuf,
}
impl FileStorage {
    pub fn new(path: impl Into<PathBuf>) -> Self {
        FileStorage { path: path.into() }
    }
    
    // 保存任务数据到文件
    pub fn save(&self, tasks: &HashMap<u32, Task>, next_id: u32) -> Result<(), StorageError> {
        let storage = TaskStorage {
            tasks: tasks.clone(),
            next_id,
        };
        
        // 序列化为JSON
        let data = serde_json::to_vec(&storage)?;
        
        // 写入文件
        let mut file = File::create(&self.path)?;
        file.write_all(&data)?;
        
        Ok(())
    }
    
    // 从文件加载任务数据
    pub fn load(&self) -> Result<(HashMap<u32, Task>, u32), StorageError> {
        // 文件不存在时返回空任务集合和next_id=1
        if !self.path.exists() {
            return Ok((HashMap::new(), 1));
        }
        
        // 读取文件内容
        let mut file = File::open(&self.path)?;
        let mut data = Vec::new();
        file.read_to_end(&mut data)?;
        
        // 反序列化JSON
        let storage: TaskStorage = serde_json::from_slice(&data)?;
        
        Ok((storage.tasks, storage.next_id))
    }
}

4. 更新 TaskManager 使用存储模块

修改cli.rs中的TaskManager,使其使用FileStorage进行任务数据的持久化。

rust 复制代码
// src/cli.rs
use crate::storage::{FileStorage, StorageError};
use crate::task::{Task, TaskStatus};
use std::collections::HashMap;
use std::time::SystemTime;
use std::path::PathBuf;
pub struct TaskManager {
    tasks: HashMap<u32, Task>,
    next_id: u32,
    storage: FileStorage,
}
impl TaskManager {
    // 从文件创建TaskManager实例
    pub fn new(storage_path: PathBuf) -> Result<Self, StorageError> {
        let storage = FileStorage::new(storage_path);
        let (tasks, next_id) = storage.load()?;
        
        Ok(TaskManager {
            tasks,
            next_id,
            storage,
        })
    }
    
    // 添加新任务
    pub fn add_task(&mut self, title: String, description: String, due_date: SystemTime) {
        let task = Task::new(
            self.next_id,
            title,
            description,
            due_date,
        );
        self.tasks.insert(self.next_id, task);
        println!("任务已添加,ID: {}", self.next_id);
        self.next_id += 1;
    }
    
    // 列出所有任务
    pub fn list_tasks(&self) {
        if self.tasks.is_empty() {
            println!("暂无任务");
            return;
        }
        
        println!("ID\t状态\t标题\t\t截止日期");
        println!("----------------------------------------");
        
        for (_, task) in &self.tasks {
            let status = match task.status {
                TaskStatus::Todo => "待办",
                TaskStatus::InProgress => "进行中",
                TaskStatus::Completed => "已完成",
            };
            // 转换截止日期为字符串
            let due_date = match task.due_date.duration_since(SystemTime::UNIX_EPOCH) {
                Ok(dur) => format!("{}", dur.as_secs()),
                Err(_) => "未知日期".to_string(),
            };
            println!("{}\t{}\t{}\t{}", task.id, status, task.title, due_date);
        }
    }
    
    // 保存任务数据到文件
    pub fn save(&self) -> Result<(), StorageError> {
        self.storage.save(&self.tasks, self.next_id)
    }
}

5. 更新 main.rs 实现数据持久化

修改main.rs,使用FileStorage并在退出时保存任务数据。

rust 复制代码
// src/main.rs
mod task;
mod cli;
mod storage;
use cli::TaskManager;
use task::TaskStatus;
use std::io::{self, BufRead};
use std::path::PathBuf;
use std::time::SystemTime;
fn main() {
    // 设置存储文件路径
    let storage_path = PathBuf::from("tasks.json");
    
    // 创建TaskManager实例,加载任务数据
    let mut manager = match TaskManager::new(storage_path.clone()) {
        Ok(manager) => manager,
        Err(e) => {
            eprintln!("加载任务数据失败: {}", e);
            return;
        }
    };
    
    let stdin = io::stdin();
    let mut input = String::new();
    
    loop {
        println!("\n===== RustTask 命令行工具 =====");
        println!("1. 添加任务");
        println!("2. 列出所有任务");
        println!("3. 更新任务状态");
        println!("4. 保存并退出");
        println!("请输入命令 (1-4):");
        
        input.clear();
        stdin.lock().read_line(&mut input).unwrap();
        let command = input.trim().parse::<u32>().unwrap_or(0);
        
        match command {
            1 => {
                // 添加任务
                println!("请输入任务标题:");
                let mut title = String::new();
                stdin.lock().read_line(&mut title).unwrap();
                title = title.trim().to_string();
                
                println!("请输入任务描述:");
                let mut description = String::new();
                stdin.lock().read_line(&mut description).unwrap();
                description = description.trim().to_string();
                
                let due_date = SystemTime::now() + std::time::Duration::from_secs(86400);  // 24小时后
                
                manager.add_task(title, description, due_date);
            },
            2 => {
                // 列出任务
                manager.list_tasks();
            },
            3 => {
                // 更新任务状态
                println!("请输入任务ID:");
                let mut id_str = String::new();
                stdin.lock().read_line(&mut id_str).unwrap();
                let task_id = id_str.trim().parse::<u32>().unwrap_or(0);
                
                if !manager.tasks.contains_key(&task_id) {
                    println!("ID为{}的任务不存在", task_id);
                    continue;
                }
                
                println!("请输入新状态 (1: 待办, 2: 进行中, 3: 已完成):");
                let mut status_str = String::new();
                stdin.lock().read_line(&mut status_str).unwrap();
                let status = status_str.trim().parse::<u32>().unwrap_or(0);
                
                let new_status = match status {
                    1 => TaskStatus::Todo,
                    2 => TaskStatus::InProgress,
                    3 => TaskStatus::Completed,
                    _ => {
                        println!("无效状态");
                        continue;
                    }
                };
                
                if let Some(task) = manager.tasks.get_mut(&task_id) {
                    task.update_status(new_status);
                    println!("任务状态已更新");
                }
            },
            4 => {
                // 保存并退出
                if let Err(e) = manager.save() {
                    eprintln!("保存任务数据失败: {}", e);
                }
                println!("感谢使用,再见!");
                break;
            },
            _ => {
                println!("无效命令,请重试");
            }
        }
    }
}

6. 编译与测试

编译并运行程序,测试任务数据的添加、更新和保存功能:

arduino 复制代码
cargo build
cargo run

程序运行示例

rust 复制代码
===== RustTask 命令行工具 =====
1. 添加任务
2. 列出所有任务
3. 更新任务状态
4. 保存并退出
请输入命令 (1-4):
1
请输入任务标题:
学习Rust
请输入任务描述:
完成第四章内容
任务已添加,ID: 1
===== RustTask 命令行工具 =====
1. 添加任务
2. 列出所有任务
3. 更新任务状态
4. 保存并退出
请输入命令 (1-4):
2
ID        状态        标题                截止日期
----------------------------------------
1        待办        学习Rust        1687680000
===== RustTask 命令行工具 =====
1. 添加任务
2. 列出所有任务
3. 更新任务状态
4. 保存并退出
请输入命令 (1-4):
3
请输入任务ID:
1
请输入新状态 (1: 待办, 2: 进行中, 3: 已完成):
2
任务状态已更新
===== RustTask 命令行工具 =====
1. 添加任务
2. 列出所有任务
3. 更新任务状态
4. 保存并退出
请输入命令 (1-4):
2
ID        状态        标题                截止日期
----------------------------------------
1        进行中        学习Rust        1687680000
===== RustTask 命令行工具 =====
1. 添加任务
2. 列出所有任务
3. 更新任务状态
4. 保存并退出
请输入命令 (1-4):
4
感谢使用,再见!

实践作业

实现任务数据的 CSV 格式导出功能,处理不同格式转换的错误,具体要求:

  1. storage.rs中添加export_to_csv方法,将任务数据导出为 CSV 格式
  1. 处理 CSV 格式转换过程中可能出现的错误
  1. 在命令行菜单中添加导出 CSV 选项(命令 5)
  1. 实现导出文件路径的用户输入功能
  1. 测试 CSV 导出功能
rust 复制代码
// 在storage.rs中添加export_to_csv方法
// 在main.rs中添加导出CSV的交互逻辑
fn main() {
    // 测试CSV导出功能
}

通过完成这个作业,你将进一步巩固模块设计、错误处理和文件操作的知识,学习如何实现不同数据格式的转换功能。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax