Rust错误处理与测试——打造健壮可维护应用的核心实践

一、学习目标与重点

1.1 学习目标

  1. 掌握错误处理基础 :理解Result类型的核心作用,熟练运用?运算符、match表达式、if let对错误进行处理与传播
  2. 精通自定义错误类型:深入学习std::error::Error trait的实现方法,构建完整的错误链,提供友好的错误信息
  3. 优化错误传播方式 :了解?运算符的原理,掌握自定义类型对?运算符的支持,遵循错误处理的最佳实践
  4. 完善测试体系:熟练编写单元测试(#[test]宏)、集成测试(tests/目录)、文档测试(///注释代码块)
  5. 实战测试开发:结合真实场景编写密码验证库、CSV解析器,包含详细的错误处理和完整的测试用例

1.2 学习重点

💡 三大核心难点

  1. 自定义错误类型的设计:如何根据业务需求定义包含足够信息的错误类型,如错误代码、错误信息、错误来源
  2. 错误链的构建与打印:如何实现source方法来连接多个错误,以及如何使用{:?}或{:#?}打印完整的错误链
  3. 测试用例的覆盖率:如何编写全面的测试用例,覆盖正常场景、边界条件、错误场景,提高代码的可靠性

⚠️ 三大高频错误点

  1. ?运算符的误用:在不返回Result类型的函数中使用?运算符,导致编译错误
  2. 测试断言的类型不匹配:assert_eq!或assert_ne!的左右操作数类型不一致,导致测试失败
  3. 忽略错误导致的程序崩溃:使用unwrap()或expect()处理所有错误,在生产环境中容易导致程序崩溃

二、错误处理基础

Rust的错误处理机制是显式的,它要求开发者必须处理所有可能的错误,避免了C语言中常见的[空指针异常]和未定义行为。

2.1 Result类型的详解

Result类型是Rust中最常用的错误处理类型,它有两个变体:

  • Ok(T):表示操作成功,包含成功值T
  • Err(E):表示操作失败,包含错误值E

⌨️ Result类型的基本操作示例

rust 复制代码
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;

// 读取文件内容的函数,返回Result<String, Box<dyn std::error::Error>>
fn read_file_content(file_path: PathBuf) -> Result<String, Box<dyn std::error::Error>> {
    // 尝试打开文件(返回Result<File, io::Error>)
    let mut file = File::open(&file_path)?; // 使用?运算符传播错误

    let mut content = String::new();
    // 尝试读取文件内容(返回Result<usize, io::Error>)
    file.read_to_string(&mut content)?;

    Ok(content)
}

fn main() {
    let file_path = PathBuf::from("nonexistent.txt");

    // 方法1:使用match表达式处理Result
    match read_file_content(file_path.clone()) {
        Ok(content) => println!("文件内容: {}", content),
        Err(e) => println!("错误1: {}", e),
    }

    // 方法2:使用if let处理Result
    if let Ok(content) = read_file_content(file_path.clone()) {
        println!("文件内容: {}", content);
    } else {
        println!("错误2: 文件不存在或无法访问");
    }

    // 方法3:使用unwrap()或expect()(生产环境不推荐)
    // let content = read_file_content(file_path).unwrap();  // panic: 线程'主'已惊慌失措
    let content = read_file_content(file_path).expect("读取文件失败");
}

AI写代码rust
运行
123456789101112131415161718192021222324252627282930313233343536

2.2 ?运算符的使用

?运算符是Rust中错误传播的简化语法,它的作用是:如果Result是Ok(T),就返回T;如果Result是Err(E),就将E转换为函数返回类型的错误类型,并提前返回函数。

⚠️ 注意事项

  1. ?运算符只能用在返回Result类型的函数中
  2. ?运算符会自动调用From trait将E转换为函数返回类型的错误类型
  3. 如果函数返回类型是Result<T, E>,那么?运算符要求E必须实现From trait

三、自定义错误类型

为了提供友好的错误信息和更好的错误处理体验,我们通常需要定义自己的错误类型

3.1 自定义错误类型的实现

自定义错误类型需要实现以下几个trait:

  1. std::error::Error:定义错误的通用接口,包含source方法(用于构建错误链)、description方法(已弃用,建议使用Display trait)
  2. std::fmt::Display:定义错误的字符串表示
  3. std::fmt::Debug:定义错误的调试表示
  4. 可选:std::convert::From trait:用于将其他类型的错误转换为自定义错误类型

⌨️ 自定义HTTP请求错误类型示例

rust 复制代码
use std::error::Error;
use std::fmt;
use reqwest::Error as ReqwestError;
use serde_json::Error as SerdeJsonError;

// 定义自定义HTTP请求错误类型
#[derive(Debug)]
enum HttpRequestError {
    // 网络错误(包含reqwest库的错误)
    NetworkError(ReqwestError),
    // JSON序列化/反序列化错误(包含serde_json库的错误)
    JsonError(SerdeJsonError),
    // HTTP状态码错误(包含状态码)
    StatusCodeError(u16),
    // 其他错误(包含错误信息)
    OtherError(String),
}

// 实现Display trait
impl fmt::Display for HttpRequestError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HttpRequestError::NetworkError(e) => write!(f, "网络错误: {}", e),
            HttpRequestError::JsonError(e) => write!(f, "JSON序列化/反序列化错误: {}", e),
            HttpRequestError::StatusCodeError(status) => write!(f, "HTTP状态码错误: {}", status),
            HttpRequestError::OtherError(msg) => write!(f, "其他错误: {}", msg),
        }
    }
}

// 实现Error trait
impl Error for HttpRequestError {
    // 实现source方法,用于构建错误链
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            HttpRequestError::NetworkError(e) => Some(e),
            HttpRequestError::JsonError(e) => Some(e),
            _ => None,
        }
    }
}

// 实现From trait,用于将ReqwestError转换为HttpRequestError
impl From<ReqwestError> for HttpRequestError {
    fn from(e: ReqwestError) -> Self {
        HttpRequestError::NetworkError(e)
    }
}

// 实现From trait,用于将SerdeJsonError转换为HttpRequestError
impl From<SerdeJsonError> for HttpRequestError {
    fn from(e: SerdeJsonError) -> Self {
        HttpRequestError::JsonError(e)
    }
}

// 发送HTTP GET请求的函数,返回Result<serde_json::Value, HttpRequestError>
async fn send_http_get_request(url: &str) -> Result<serde_json::Value, HttpRequestError> {
    let client = reqwest::Client::new();
    let response = client.get(url).send().await?;

    if response.status().is_success() {
        let json = response.json().await?;
        Ok(json)
    } else {
        Err(HttpRequestError::StatusCodeError(response.status().as_u16()))
    }
}

#[tokio::main]
async fn main() {
    let url = "https://jsonplaceholder.typicode.com/todos/1";
    // 打印完整的错误链
    if let Err(e) = send_http_get_request(url).await {
        println!("错误: {}", e);
        if let Some(source) = e.source() {
            println!("错误来源: {}", source);
        }
        println!("调试信息: {:?}", e);
    }

    let invalid_url = "invalid_url";
    if let Err(e) = send_http_get_request(invalid_url).await {
        println!("错误: {}", e);
        if let Some(source) = e.source() {
            println!("错误来源: {}", source);
        }
        println!("调试信息: {:?}", e);
    }
}

AI写代码rust
运行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990

四、单元测试

单元测试是Rust中验证代码正确性的基本方法,它测试函数或方法的输入输出是否符合预期。

4.1 测试函数的编写

测试函数需要满足以下条件:

  1. 使用#[test]宏标记
  2. 不接受参数,不返回值
  3. 可以使用测试断言函数(assert!、assert_eq!、assert_ne!)来验证结果

⌨️ 数学库函数的单元测试示例

scss 复制代码
// 数学库函数
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

pub fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("除数不能为0".to_string())
    } else {
        Ok(a / b)
    }
}

// 单元测试模块(使用#[cfg(test)]宏标记,只在测试模式下编译)
#[cfg(test)]
mod tests {
    // 导入测试模块外部的函数
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
        assert_eq!(add(-1, 5), 4);
        assert_eq!(add(0, 0), 0);
    }

    #[test]
    fn test_subtract() {
        assert_eq!(subtract(5, 2), 3);
        assert_eq!(subtract(-1, 5), -6);
        assert_eq!(subtract(0, 0), 0);
    }

    #[test]
    fn test_multiply() {
        assert_eq!(multiply(2, 3), 6);
        assert_eq!(multiply(-1, 5), -5);
        assert_eq!(multiply(0, 5), 0);
    }

    #[test]
    fn test_divide_success() {
        assert_eq!(divide(6, 2), Ok(3));
        assert_eq!(divide(-6, 3), Ok(-2));
    }

    #[test]
    #[should_panic(expected = "除数不能为0")] // 期望测试函数panic,并包含指定的错误信息
    fn test_divide_panic() {
        divide(6, 0).unwrap();
    }

    #[test]
    #[ignore] // 忽略该测试函数(不执行)
    fn test_ignore() {
        assert_eq!(1, 2); // 该断言会失败,但测试会被忽略
    }
}

AI写代码rust
运行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

4.2 运行单元测试

在项目根目录下运行以下命令来执行单元测试:

bash 复制代码
cargo test

AI写代码bash
1

运行指定测试函数:

bash 复制代码
cargo test test_add

AI写代码bash
1

运行包含指定前缀的测试函数:

bash 复制代码
cargo test test_

AI写代码bash
1

运行所有未被忽略的测试函数:

bash 复制代码
cargo test -- --ignored

AI写代码bash
1

五、集成测试

集成测试是Rust中验证代码模块之间协作正确性的方法,它测试整个库或应用程序的功能。

5.1 集成测试的目录结构

集成测试的代码通常放在项目根目录下的tests/目录中,每个测试文件都会被编译成一个单独的二进制文件。

5.2 集成测试的编写

⌨️ HTTP客户端请求工具的集成测试示例

首先,在src/lib.rs中定义HTTP客户端请求工具的库函数:

rust 复制代码
use reqwest::Client;
use serde_json::Value;

pub enum HttpMethod {
    GET,
    POST,
}

pub struct HttpRequest {
    pub url: String,
    pub method: HttpMethod,
    pub json_body: Option<Value>,
}

impl HttpRequest {
    pub fn new(url: String, method: HttpMethod, json_body: Option<Value>) -> Self {
        HttpRequest { url, method, json_body }
    }

    pub async fn send(&self) -> Result<Value, Box<dyn std::error::Error>> {
        let client = Client::new();
        let response = match self.method {
            HttpMethod::GET => client.get(&self.url).send().await?,
            HttpMethod::POST => {
                if let Some(body) = &self.json_body {
                    client.post(&self.url).json(body).send().await?
                } else {
                    return Err("POST请求需要提供JSON body".into());
                }
            }
        };

        if response.status().is_success() {
            let json = response.json().await?;
            Ok(json)
        } else {
            Err(format!("HTTP状态码错误: {}", response.status()).into())
        }
    }
}

AI写代码rust
运行
12345678910111213141516171819202122232425262728293031323334353637383940

然后,在tests/integration_test.rs中编写集成测试:

rust 复制代码
use http_client_request_tool::{HttpRequest, HttpMethod};
use serde_json::json;

#[tokio::test] // 使用tokio的test宏,因为HTTP请求是异步的
async fn test_send_http_get_request() {
    let url = "https://jsonplaceholder.typicode.com/todos/1";
    let request = HttpRequest::new(url.to_string(), HttpMethod::GET, None);
    let response = request.send().await.unwrap();

    assert_eq!(response["userId"], 1);
    assert_eq!(response["id"], 1);
    assert_eq!(response["title"], "delectus aut autem");
    assert_eq!(response["completed"], false);
}

#[tokio::test]
async fn test_send_http_post_request() {
    let url = "https://jsonplaceholder.typicode.com/todos";
    let json_body = json!({
        "userId": 1,
        "title": "test post request",
        "completed": false
    });
    let request = HttpRequest::new(url.to_string(), HttpMethod::POST, Some(json_body));
    let response = request.send().await.unwrap();

    assert_eq!(response["userId"], 1);
    assert_eq!(response["title"], "test post request");
    assert_eq!(response["completed"], false);
    assert!(response["id"].is_number());
}

#[tokio::test]
async fn test_send_http_request_with_invalid_url() {
    let url = "invalid_url";
    let request = HttpRequest::new(url.to_string(), HttpMethod::GET, None);
    let result = request.send().await;

    assert!(result.is_err());
    if let Err(e) = result {
        println!("错误信息: {}", e);
    }
}

AI写代码rust
运行
12345678910111213141516171819202122232425262728293031323334353637383940414243

5.3 运行集成测试

在项目根目录下运行以下命令来执行集成测试:

bash 复制代码
cargo test

AI写代码bash
1

六、文档测试

文档测试是Rust中验证文档示例正确性的方法,它测试///注释中的代码块是否能够正常编译和运行。

6.1 文档测试的语法

文档测试的语法是在///注释中添加代码块,代码块的语法是:

ruby 复制代码
/// ```
/// // 代码示例
/// ```

AI写代码rust
运行
123

⌨️ 数学库函数的文档测试示例

rust 复制代码
/// 计算两个整数的和
///
/// # 例子
///
/// ```
/// use math_lib::add;
///
/// let result = add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// 计算两个整数的差
///
/// # 例子
///
/// ```
/// use math_lib::subtract;
///
/// let result = subtract(5, 2);
/// assert_eq!(result, 3);
/// ```
pub fn subtract(a: i32, b: i32) -> i32 {
    a - b
}

/// 计算两个整数的积
///
/// # 例子
///
/// ```
/// use math_lib::multiply;
///
/// let result = multiply(2, 3);
/// assert_eq!(result, 6);
/// ```
pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

/// 计算两个整数的商
///
/// # 例子
///
/// ```
/// use math_lib::divide;
///
/// let result = divide(6, 2);
/// assert_eq!(result, Ok(3));
/// ```
///
/// # 错误示例
///
/// ```
/// use math_lib::divide;
///
/// let result = divide(6, 0);
/// assert!(result.is_err());
/// ```
pub fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("除数不能为0".to_string())
    } else {
        Ok(a / b)
    }
}

AI写代码rust
运行
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768

6.2 运行文档测试

在项目根目录下运行以下命令来执行文档测试:

bash 复制代码
cargo test --doc

AI写代码bash
1

七、真实案例应用

7.1 案例1:实现安全的密码验证库

💡 场景分析:需要编写一个安全的密码验证库,支持验证密码的长度、复杂度(包含大写字母、小写字母、数字、特殊字符),返回详细的错误信息,并包含完整的测试用例。

⌨️ 代码示例

rust 复制代码
use std::error::Error;
use std::fmt;

// 定义密码验证错误类型
#[derive(Debug)]
enum PasswordValidationError {
    // 密码长度不足(包含最小长度)
    LengthTooShort(usize),
    // 密码长度过长(包含最大长度)
    LengthTooLong(usize),
    // 密码不包含大写字母
    NoUppercaseLetter,
    // 密码不包含小写字母
    NoLowercaseLetter,
    // 密码不包含数字
    NoDigit,
    // 密码不包含特殊字符
    NoSpecialCharacter,
}

// 实现Display trait
impl fmt::Display for PasswordValidationError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PasswordValidationError::LengthTooShort(min) => write!(f, "密码长度不足,至少需要{}个字符", min),
            PasswordValidationError::LengthTooLong(max) => write!(f, "密码长度过长,最多需要{}个字符", max),
            PasswordValidationError::NoUppercaseLetter => write!(f, "密码必须包含至少一个大写字母"),
            PasswordValidationError::NoLowercaseLetter => write!(f, "密码必须包含至少一个小写字母"),
            PasswordValidationError::NoDigit => write!(f, "密码必须包含至少一个数字"),
            PasswordValidationError::NoSpecialCharacter => write!(f, "密码必须包含至少一个特殊字符(!@#$%^&*()_+-=[]{}|;:,.<>?)"),
        }
    }
}

// 实现Error trait
impl Error for PasswordValidationError {}

// 密码验证配置
struct PasswordValidationConfig {
    min_length: usize,
    max_length: usize,
    require_uppercase: bool,
    require_lowercase: bool,
    require_digit: bool,
    require_special: bool,
}

impl Default for PasswordValidationConfig {
    fn default() -> Self {
        PasswordValidationConfig {
            min_length: 8,
            max_length: 32,
            require_uppercase: true,
            require_lowercase: true,
            require_digit: true,
            require_special: true,
        }
    }
}

impl PasswordValidationConfig {
    fn new() -> Self {
        Self::default()
    }

    fn with_min_length(mut self, min_length: usize) -> Self {
        self.min_length = min_length;
        self
    }

    fn with_max_length(mut self, max_length: usize) -> Self {
        self.max_length = max_length;
        self
    }

    fn without_uppercase(mut self) -> Self {
        self.require_uppercase = false;
        self
    }

    fn without_lowercase(mut self) -> Self {
        self.require_lowercase = false;
        self
    }

    fn without_digit(mut self) -> Self {
        self.require_digit = false;
        self
    }

    fn without_special(mut self) -> Self {
        self.require_special = false;
        self
    }
}

// 密码验证函数
fn validate_password(password: &str, config: &PasswordValidationConfig) -> Result<(), PasswordValidationError> {
    // 验证密码长度
    if password.len() < config.min_length {
        return Err(PasswordValidationError::LengthTooShort(config.min_length));
    }

    if password.len() > config.max_length {
        return Err(PasswordValidationError::LengthTooLong(config.max_length));
    }

    // 验证密码复杂度
    if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
        return Err(PasswordValidationError::NoUppercaseLetter);
    }

    if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
        return Err(PasswordValidationError::NoLowercaseLetter);
    }

    if config.require_digit && !password.chars().any(|c| c.is_numeric()) {
        return Err(PasswordValidationError::NoDigit);
    }

    if config.require_special && !password.chars().any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)) {
        return Err(PasswordValidationError::NoSpecialCharacter);
    }

    Ok(())
}

// 单元测试模块
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_password_with_default_config_success() {
        let password = "P@ssw0rd123";
        let config = PasswordValidationConfig::new();
        assert!(validate_password(password, &config).is_ok());
    }

    #[test]
    fn test_validate_password_with_custom_config_success() {
        let password = "passw0rd";
        let config = PasswordValidationConfig::new()
            .without_uppercase()
            .without_special();
        assert!(validate_password(password, &config).is_ok());
    }

    #[test]
    fn test_validate_password_length_too_short() {
        let password = "P@ssw0r";
        let config = PasswordValidationConfig::new();
        let result = validate_password(password, &config);
        assert!(result.is_err());
        if let Err(e) = result {
            assert!(matches!(e, PasswordValidationError::LengthTooShort(8)));
        }
    }

    #[test]
    fn test_validate_password_no_uppercase() {
        let password = "p@ssw0rd123";
        let config = PasswordValidationConfig::new();
        let result = validate_password(password, &config);
        assert!(result.is_err());
        if let Err(e) = result {
            assert!(matches!(e, PasswordValidationError::NoUppercaseLetter));
        }
    }

    #[test]
    fn test_validate_password_no_special() {
        let password = "Password123";
        let config = PasswordValidationConfig::new();
        let result = validate_password(password, &config);
        assert!(result.is_err());
        if let Err(e) = result {
            assert!(matches!(e, PasswordValidationError::NoSpecialCharacter));
        }
    }
}

fn main() {
    let password = "P@ssw0rd123";
    let config = PasswordValidationConfig::new();
    match validate_password(password, &config) {
        Ok(_) => println!("密码验证成功"),
        Err(e) => println!("密码验证失败: {}", e),
    }
}

AI写代码rust
运行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190

7.2 案例2:实现简单的CSV解析器

💡 场景分析:需要编写一个简单的CSV解析器,支持解析CSV文件和字符串,返回一个二维数组,处理解析过程中可能出现的错误,并包含完整的测试用例。

⌨️ 代码示例

rust 复制代码
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::{Read, BufRead, BufReader};
use std::path::PathBuf;

// 定义CSV解析错误类型
#[derive(Debug)]
enum CsvParseError {
    // 文件操作错误(包含io::Error)
    FileError(std::io::Error),
    // CSV行格式错误(包含行号和错误信息)
    LineFormatError(usize, String),
    // CSV列数不一致错误(包含期望列数和实际列数)
    ColumnCountError(usize, usize),
}

// 实现Display trait
impl fmt::Display for CsvParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CsvParseError::FileError(e) => write!(f, "文件操作错误: {}", e),
            CsvParseError::LineFormatError(line, msg) => write!(f, "第{}行格式错误: {}", line, msg),
            CsvParseError::ColumnCountError(expected, actual) => write!(f, "列数不一致错误: 期望{}列,实际{}列", expected, actual),
        }
    }
}

// 实现Error trait
impl Error for CsvParseError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            CsvParseError::FileError(e) => Some(e),
            _ => None,
        }
    }
}

// 实现From trait,用于将io::Error转换为CsvParseError
impl From<std::io::Error> for CsvParseError {
    fn from(e: std::io::Error) -> Self {
        CsvParseError::FileError(e)
    }
}

// CSV解析配置
struct CsvParseConfig {
    delimiter: char,
    has_header: bool,
}

impl Default for CsvParseConfig {
    fn default() -> Self {
        CsvParseConfig {
            delimiter: ',',
            has_header: false,
        }
    }
}

impl CsvParseConfig {
    fn new() -> Self {
        Self::default()
    }

    fn with_delimiter(mut self, delimiter: char) -> Self {
        self.delimiter = delimiter;
        self
    }

    fn with_header(mut self) -> Self {
        self.has_header = true;
        self
    }
}

// CSV解析函数
fn parse_csv_string(csv: &str, config: &CsvParseConfig) -> Result<Vec<Vec<String>>, CsvParseError> {
    let mut lines = csv.lines();
    let mut result = Vec::new();
    let mut expected_columns = None;
    let mut line_number = 0;

    // 跳过表头
    if config.has_header {
        if let Some(line) = lines.next() {
            line_number += 1;
            let row = parse_csv_line(line, config.delimiter, line_number)?;
            expected_columns = Some(row.len());
            result.push(row);
        }
    }

    // 解析数据行
    while let Some(line) = lines.next() {
        line_number += 1;
        let row = parse_csv_line(line, config.delimiter, line_number)?;

        // 验证列数是否一致
        if let Some(expected) = expected_columns {
            if row.len() != expected {
                return Err(CsvParseError::ColumnCountError(expected, row.len()));
            }
        } else {
            expected_columns = Some(row.len());
        }

        result.push(row);
    }

    Ok(result)
}

// 解析CSV行的辅助函数
fn parse_csv_line(line: &str, delimiter: char, line_number: usize) -> Result<Vec<String>, CsvParseError> {
    let mut row = Vec::new();
    let mut current_cell = String::new();
    let mut in_quote = false;

    for c in line.chars() {
        match c {
            '"' => in_quote = !in_quote,
            d if d == delimiter && !in_quote => {
                row.push(current_cell.trim().to_string());
                current_cell.clear();
            },
            _ => current_cell.push(c),
        }
    }

    row.push(current_cell.trim().to_string());

    if in_quote {
        return Err(CsvParseError::LineFormatError(line_number, "引号未闭合".to_string()));
    }

    Ok(row)
}

// 解析CSV文件的函数
fn parse_csv_file(file_path: PathBuf, config: &CsvParseConfig) -> Result<Vec<Vec<String>>, CsvParseError> {
    let file = File::open(file_path)?;
    let reader = BufReader::new(file);
    let mut csv = String::new();

    for line_result in reader.lines() {
        let line = line_result?;
        csv.push_str(&line);
        csv.push('\n');
    }

    parse_csv_string(&csv, config)
}

// 单元测试模块
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_csv_string_with_default_config() {
        let csv = "1,张三,18\n2,李四,20\n3,王五,22";
        let config = CsvParseConfig::new();
        let result = parse_csv_string(csv, &config).unwrap();

        assert_eq!(result.len(), 3);
        assert_eq!(result[0], vec!["1", "张三", "18"]);
        assert_eq!(result[1], vec!["2", "李四", "20"]);
        assert_eq!(result[2], vec!["3", "王五", "22"]);
    }

    #[test]
    fn test_parse_csv_string_with_header() {
        let csv = "ID,姓名,年龄\n1,张三,18\n2,李四,20\n3,王五,22";
        let config = CsvParseConfig::new().with_header();
        let result = parse_csv_string(csv, &config).unwrap();

        assert_eq!(result.len(), 4);
        assert_eq!(result[0], vec!["ID", "姓名", "年龄"]);
        assert_eq!(result[1], vec!["1", "张三", "18"]);
    }

    #[test]
    fn test_parse_csv_string_with_semicolon_delimiter() {
        let csv = "1;张三;18\n2;李四;20\n3;王五;22";
        let config = CsvParseConfig::new().with_delimiter(';');
        let result = parse_csv_string(csv, &config).unwrap();

        assert_eq!(result.len(), 3);
        assert_eq!(result[0], vec!["1", "张三", "18"]);
    }

    #[test]
    fn test_parse_csv_string_with_quoted_cell() {
        let csv = "1,"张三,李四",18\n2,王五,20";
        let config = CsvParseConfig::new();
        let result = parse_csv_string(csv, &config).unwrap();

        assert_eq!(result.len(), 2);
        assert_eq!(result[0], vec!["1", "张三,李四", "18"]);
    }

    #[test]
    fn test_parse_csv_string_column_count_error() {
        let csv = "1,张三,18\n2,李四\n3,王五,22";
        let config = CsvParseConfig::new();
        let result = parse_csv_string(csv, &config);

        assert!(result.is_err());
        if let Err(e) = result {
            assert!(matches!(e, CsvParseError::ColumnCountError(3, 2)));
        }
    }

    #[test]
    fn test_parse_csv_string_quote_error() {
        let csv = "1,"张三,李四,18\n2,王五,20";
        let config = CsvParseConfig::new();
        let result = parse_csv_string(csv, &config);

        assert!(result.is_err());
        if let Err(e) = result {
            assert!(matches!(e, CsvParseError::LineFormatError(1, _)));
        }
    }
}

fn main() {
    let csv = "ID,姓名,年龄\n1,张三,18\n2,李四,20\n3,王五,22";
    let config = CsvParseConfig::new().with_header();
    match parse_csv_string(csv, &config) {
        Ok(result) => {
            println!("CSV解析成功");
            for row in result {
                println!("{:?}", row);
            }
        },
        Err(e) => println!("CSV解析失败: {}", e),
    }
}

AI写代码rust
运行
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240

八、常见问题与解决方案

8.1 ?运算符的误用

问题现象:在不返回Result类型的函数中使用?运算符,导致编译错误。

解决方案

  1. 确保使用?运算符的函数返回Result类型
  2. 如果函数需要返回其他类型,可以使用match表达式或if let处理Result
  3. 在main函数中使用?运算符,需要将main函数的返回类型改为Result<(), Box>

8.2 测试断言的类型不匹配

问题现象:assert_eq!或assert_ne!的左右操作数类型不一致,导致测试失败。

解决方案

  1. 检查左右操作数的类型是否一致
  2. 如果类型不一致,可以使用类型转换函数(如as u32)
  3. 使用{:?}打印调试信息,查看类型不匹配的原因

8.3 忽略错误导致的程序崩溃

问题现象:使用unwrap()或expect()处理所有错误,在生产环境中容易导致程序崩溃。

解决方案

  1. 对于生产环境中的代码,应该使用match表达式或if let处理所有错误
  2. 提供友好的错误信息,帮助用户定位问题
  3. 使用日志库(如log、env_logger)记录错误信息

九、总结与展望

9.1 总结

掌握了错误处理基础 :理解了Result类型的核心作用,熟练运用了?运算符、match表达式、if let对错误进行处理与传播

精通了自定义错误类型 :深入学习了std::error::Error trait的实现方法,构建了完整的错误链,提供了友好的错误信息

优化了错误传播方式 :了解了?运算符的原理,掌握了自定义类型对?运算符的支持,遵循了错误处理的最佳实践

完善了测试体系 :熟练编写了单元测试(#[test]宏)、集成测试(tests/目录)、文档测试(///注释代码块)

实战测试开发:结合真实场景编写了两个实用的代码案例:安全的密码验证库和简单的CSV解析器,包含详细的错误处理和完整的测试用例

9.2 展望

下一篇文章,我们将深入学习Rust的异步编程进阶,包括Tokio库的任务调度、流的处理、超时设置、连接池,通过这些知识我们将能够编写更高效的异步应用程序,如Web服务器、客户端、数据处理工具。

相关推荐
青云计划11 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor35611 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor35611 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11113 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
艾尔aier13 小时前
mini-shell成果展示
rust
Tony Bai13 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
+VX:Fegn089513 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟14 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事14 小时前
SQLAlchemy 技术入门指南
后端·python
识君啊14 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端