第四章:模块化设计与错误处理
教学目标
- 掌握 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 格式导出功能,处理不同格式转换的错误,具体要求:
- 在storage.rs中添加export_to_csv方法,将任务数据导出为 CSV 格式
- 处理 CSV 格式转换过程中可能出现的错误
- 在命令行菜单中添加导出 CSV 选项(命令 5)
- 实现导出文件路径的用户输入功能
- 测试 CSV 导出功能
rust
// 在storage.rs中添加export_to_csv方法
// 在main.rs中添加导出CSV的交互逻辑
fn main() {
// 测试CSV导出功能
}
通过完成这个作业,你将进一步巩固模块设计、错误处理和文件操作的知识,学习如何实现不同数据格式的转换功能。