Day 9: 错误处理
今天是《30天精通Rust语言》系列的第九天。我们将学习Rust的错误处理机制。Rust采用显式错误处理策略,没有异常机制,所有可能失败的操作用 Result 类型表示,需要显式处理错误。
2. Result 类型
Section titled “2. Result 类型”Result 是Rust标准库中用于表示操作结果的枚举类型:
enum Result<T, E> { Ok(T), // 成功,持有成功值 Err(E), // 失败,持有错误值}与 Option<T> 类似,但 Result 可以携带错误信息。
2.1 基本使用
Section titled “2.1 基本使用”use std::fs::File;use std::io::Read;
fn main() { let result = File::open("hello.txt");
match result { Ok(mut file) => { let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); println!("File contents: {}", contents); } Err(error) => { println!("Error opening file: {:?}", error); } }}2.2 常见的Result方法
Section titled “2.2 常见的Result方法”fn main() { let result: Result<i32, &str> = Ok(42);
// unwrap:成功返回Ok的值,失败panic let n = result.unwrap(); println!("n = {}", n);
// unwrap_or:失败时返回默认值 let result: Result<i32, &str> = Err("error"); let n = result.unwrap_or(0); println!("n = {}", n);
// is_ok和is_err:判断结果类型 let result: Result<i32, &str> = Ok(42); println!("is_ok: {}, is_err: {}", result.is_ok(), result.is_err());}3. ? 运算符
Section titled “3. ? 运算符”? 运算符是Rust中处理错误的利器,可以简化错误传播:
3.1 未使用?的情况
Section titled “3.1 未使用?的情况”use std::fs::File;use std::io::Read;
fn read_file_contents(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; // ?运算符
let mut contents = String::new(); file.read_to_string(&mut contents)?;
Ok(contents)}
fn main() { match read_file_contents("hello.txt") { Ok(contents) => println!("{}", contents), Err(e) => println!("Error: {:?}", e), }}3.2 ?的工作原理
Section titled “3.2 ?的工作原理”? 运算符的展开等价于以下 match 表达式:
// ?运算符写法let mut file = File::open(path)?;
// 等价于let mut file = match File::open(path) { Ok(file) => file, Err(e) => return Err(e.into()),};如果结果是 Ok,展开并继续执行;如果是 Err,提前返回,将错误传播给调用者。
4. panic!宏
Section titled “4. panic!宏”当程序遇到无法恢复的错误时,使用 panic! 终止程序:
fn main() { panic!("This is a critical error!"); println!("This will never be printed");}运行时会输出:
thread 'main' panicked at 'This is a critical error!', src/main.rs:2:5note: run with `RUST_BACKTRACE=1` ...4.1 何时使用panic
Section titled “4.1 何时使用panic”- 不可恢复错误:如预期不可能发生的情况(使用
unreachable!更好)。 - 原型开发:快速原型中可使用
unwrap(),生产代码应使用?。 - 测试代码:测试中验证某操作应该失败。
4.2 unwrap和expect
Section titled “4.2 unwrap和expect”在开发中快速获取值的方法:
fn main() { let result: Option<i32> = Some(42);
// unwrap:Some返回值,None panic let n = result.unwrap(); println!("n = {}", n);
// expect:提供错误信息 let n = result.expect("Expected a value"); println!("n = {}", n);}5. 自定义错误类型
Section titled “5. 自定义错误类型”对于实际应用,你应该定义自己的错误类型:
use std::fmt;use std::num::ParseIntError;
// 定义错误类型#[derive(Debug)]enum AppError { IoError(std::io::Error), ParseError(ParseIntError), InvalidInput(String),}
//实现Display traitimpl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::IoError(e) => write!(f, "IO error: {}", e), AppError::ParseError(e) => write!(f, "Parse error: {}", e), AppError::InvalidInput(s) => write!(f, "Invalid input: {}", s), } }}
// 实现Error traitimpl std::error::Error for AppError {}
// From trait用于?运算符自动转换impl From<std::io::Error> for AppError { fn from(err: std::io::Error) -> Self { AppError::IoError(err) }}
impl From<ParseIntError> for AppError { fn from(err: ParseIntError) -> Self { AppError::ParseError(err) }}
fn parse_and_double(s: &str) -> Result<i32, AppError> { let n: i32 = s.trim().parse::<i32>()?; if n < 0 { return Err(AppError::InvalidInput("Negative numbers not allowed".to_string())); } Ok(n * 2)}
fn main() { let cases = ["42", "hello", "-5"];
for s in cases { match parse_and_double(s) { Ok(n) => println!("{} * 2 = {}", s, n), Err(e) => println!("Error processing '{}': {}", s, e), } }}6. 错误处理的最佳实践
Section titled “6. 错误处理的最佳实践”6.1 组合器
Section titled “6.1 组合器”Result 类型提供了丰富的组合器方法:
fn main() { let result: Result<i32, &str> = Ok(5);
// map:转换Ok值 let doubled = result.map(|x| x * 2); println!("doubled: {:?}", doubled);
// map_err:转换Err值 let result: Result<i32, &str> = Err("error"); let mapped = result.map_err(|e| e.len()); println!("mapped: {:?}", mapped);
// and_then:链式处理 let result: Result<i32, &str> = Ok(5); let chained = result.and_then(|x| Ok(x * 2)); println!("chained: {:?}", chained);}6.2 使用anyhow简化错误处理
Section titled “6.2 使用anyhow简化错误处理”对于应用程序,anyhow crate 提供了更灵活的错误处理:
[dependencies]anyhow = "1.0"use anyhow::Result;
fn read_config(path: &str) -> Result<String> { let contents = std::fs::read_to_string(path)?; Ok(contents)}
fn main() -> Result<()> { let config = read_config("config.toml")?; println!("Config: {}", config); Ok(())}7. 实践练习
Section titled “7. 实践练习”今天的实践任务:
- 编写程序打开一个不存在的文件,使用
match打印成功或失败消息。 - 使用
?运算符重写上述程序。 - 创建一个自定义错误类型
DivisionError,包含”除以零”和”结果非整数”两种错误。 - 实现一个字符串解析函数,解析后使用
?传播错误。 - 使用
Result的组合器方法map和and_then实现链式计算。
8. 常见问题
Section titled “8. 常见问题”问:什么时候应该panic而不是返回Result?
答:当错误表示程序中存在无法恢复的状态时使用panic,如数组越界(已由Rust标准库检查)、契约违反等。返回Result适用于调用者可能需要处理的错误。
问:?运算符能否用于Option?
答:可以,但需要确认值存在时才能用。? 用于Option时会尝试获取值,如果为None则提前返回None。
今天我们学习了Rust的错误处理机制:
Result<T, E>是表示可能失败操作的枚举类型。?运算符简化了错误的传播和处理。panic!用于不可恢复的错误,终止程序。- 自定义错误类型提供了更丰富的错误信息。
- 组合器和第三方库(如
anyhow)简化了错误处理代码。
明天我们将学习Rust的最后一个核心概念:生命周期。加油!