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

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

教学目标

  • 掌握 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导出功能
}

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

相关推荐
Cacciatore->几秒前
React 基本介绍与项目创建
前端·react.js·arcgis
摸鱼仙人~2 分钟前
React Ref 指南:原理、实现与实践
前端·javascript·react.js
teeeeeeemo4 分钟前
回调函数 vs Promise vs async/await区别
开发语言·前端·javascript·笔记
贵沫末22 分钟前
React——基础
前端·react.js·前端框架
aklry34 分钟前
uniapp三步完成一维码的生成
前端·vue.js
Rubin9341 分钟前
判断元素在可视区域?用于滚动加载,数据埋点等
前端
爱学习的茄子41 分钟前
AI驱动的单词学习应用:从图片识别到语音合成的完整实现
前端·深度学习·react.js
用户38022585982442 分钟前
使用three.js实现3D地球
前端·three.js
程序无bug44 分钟前
Spring 面向切面编程AOP 详细讲解
java·前端
zhanshuo44 分钟前
鸿蒙UI开发全解:JS与Java双引擎实战指南
前端·javascript·harmonyos