Rust Option and Result key takeaways
Rust 里 Option 与 Result 的关键结论
What you’ll learn: Idiomatic error handling patterns — safe alternatives to
unwrap(), the?operator for propagation, custom error types, and when to useanyhowvsthiserrorin production code.
本章将学到什么: 惯用的错误处理模式,unwrap()的安全替代方案,?的错误传播方式,自定义错误类型的设计,以及生产代码里什么时候该用anyhow、什么时候该用thiserror。
OptionandResultare an integral part of idiomatic Rust.Option和Result是 Rust 惯用写法的核心组成部分。- Safe alternatives to
unwrap():unwrap()的安全替代方案:
#![allow(unused)]
fn main() {
// Option<T> safe alternatives
// Option<T> 的安全替代写法
let value = opt.unwrap_or(default); // Provide fallback value
let value = opt.unwrap_or_else(|| compute()); // Lazy computation for fallback
let value = opt.unwrap_or_default(); // Use Default trait implementation
let value = opt.expect("descriptive message"); // Only when panic is acceptable
// Result<T, E> safe alternatives
// Result<T, E> 的安全替代写法
let value = result.unwrap_or(fallback); // Ignore error, use fallback
let value = result.unwrap_or_else(|e| handle(e)); // Handle error, return fallback
let value = result.unwrap_or_default(); // Use Default trait
}
- Pattern matching for explicit control:
需要显式控制时,用模式匹配:
#![allow(unused)]
fn main() {
match some_option {
Some(value) => println!("Got: {}", value),
None => println!("No value found"),
}
match some_result {
Ok(value) => process(value),
Err(error) => log_error(error),
}
}
- Use
?operator for error propagation: Short-circuit and bubble up errors.
用?传播错误:遇到错误立刻短路,并把错误往上返回。
#![allow(unused)]
fn main() {
fn process_file(path: &str) -> Result<String, std::io::Error> {
let content = std::fs::read_to_string(path)?; // Automatically returns error
Ok(content.to_uppercase())
}
}
- Transformation methods:
常见变换方法:map(): Transform the success valueOk(T)->Ok(U)orSome(T)->Some(U)map():变换成功值,把Ok(T)变成Ok(U),或者把Some(T)变成Some(U)。map_err(): Transform the error typeErr(E)->Err(F)map_err():变换错误类型,把Err(E)变成Err(F)。and_then(): Chain operations that can failand_then():把一串可能失败的操作接起来。
- Use in your own APIs: Prefer
Result<T, E>over exceptions or error codes.
写自己的 API 时,优先返回Result<T, E>,别把异常和错误码那套老习惯又拖回来。 - References: Option docs | Result docs
参考资料: Option 文档 | Result 文档
Rust Common Pitfalls and Debugging Tips
Rust 常见误区与排查提示
- Borrowing issues: Most common beginner mistake.
借用问题:这是新手最常踩的一类错误。"cannot borrow as mutable"-> Only one mutable reference allowed at a time"cannot borrow as mutable":同一时间只允许存在一个可变引用。"borrowed value does not live long enough"-> Reference outlives the data it points to"borrowed value does not live long enough":引用活得比它指向的数据还久。- Fix: Use scopes
{}to limit reference lifetimes, or clone data when needed.
处理方式: 用{}缩短引用作用域,或者在确实有必要时复制数据。
- Missing trait implementations:
"method not found"errors.
缺少 trait 实现:经常会炸出"method not found"这种报错。- Fix: Add
#[derive(Debug, Clone, PartialEq)]for common traits.
处理方式: 常用 trait 可以先补上#[derive(Debug, Clone, PartialEq)]。 - Use
cargo checkto get better error messages thancargo run.cargo check给出的错误通常比cargo run更聚焦。
- Fix: Add
- Integer overflow in debug mode: Rust panics on overflow.
调试模式下整数溢出:Rust 遇到溢出会直接 panic。- Fix: Use
wrapping_add(),saturating_add(), orchecked_add()for explicit behavior.
处理方式: 用wrapping_add()、saturating_add()或checked_add()明确指定溢出语义。
- Fix: Use
- String vs
&strconfusion: Different types for different use cases.String和&str容易搞混:两者本来就是给不同场景准备的。- Use
&strfor string slices (borrowed),Stringfor owned strings.&str适合借用的字符串切片,String适合拥有所有权的字符串。 - Fix: Use
.to_string()orString::from()to convert&strtoString.
处理方式: 用.to_string()或String::from()把&str转成String。
- Use
- Fighting the borrow checker: Stop trying to outsmart it.
跟借用检查器对着干:这事十有八九干不过,别硬拧。- Fix: Restructure code to work with ownership rules rather than against them.
处理方式: 调整代码结构,让它顺着所有权规则走。 - Consider using
Rc<RefCell<T>>for complex sharing scenarios, but use it sparingly.
特别复杂的共享场景可以考虑Rc<RefCell<T>>,但用多了代码就容易发黏。
- Fix: Restructure code to work with ownership rules rather than against them.
Error Handling Examples: Good vs Bad
错误处理示例:好写法与坏写法
#![allow(unused)]
fn main() {
// [ERROR] BAD: Can panic unexpectedly
// [ERROR] 坏写法:随时可能猝不及防地 panic
fn bad_config_reader() -> String {
let config = std::env::var("CONFIG_FILE").unwrap(); // Panic if not set!
std::fs::read_to_string(config).unwrap() // Panic if file missing!
}
// [OK] GOOD: Handles errors gracefully
// [OK] 好写法:对错误做了正常处理
fn good_config_reader() -> Result<String, ConfigError> {
let config_path = std::env::var("CONFIG_FILE")
.unwrap_or_else(|_| "default.conf".to_string()); // Fallback to default
let content = std::fs::read_to_string(config_path)
.map_err(ConfigError::FileRead)?; // Convert and propagate error
Ok(content)
}
// [OK] EVEN BETTER: With proper error types
// [OK] 更进一步:定义清楚的错误类型
use thiserror::Error;
#[derive(Error, Debug)]
enum ConfigError {
#[error("Failed to read config file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid configuration: {message}")]
Invalid { message: String },
}
}
Let’s break down what’s happening here. ConfigError has just two variants — one for I/O errors and one for validation errors. This is the right starting point for most modules:
拆开看一下这里的意思。ConfigError 只有 两个变体,一个表示 I/O 错误,一个表示校验错误。对大多数模块来说,这样的起步规模就够用了。
ConfigError variantConfigError 变体 | Holds 保存内容 | Created by 创建来源 |
|---|---|---|
FileRead(io::Error) | The original I/O error 原始 I/O 错误 | #[from] auto-converts via ?通过 #[from] 配合 ? 自动转换 |
Invalid { message } | A human-readable explanation 给人看的说明文本 | Your validation code 业务校验逻辑自己构造 |
Now you can write functions that return Result<T, ConfigError>:
这样后面的函数就可以统一返回 Result<T, ConfigError>:
#![allow(unused)]
fn main() {
fn read_config(path: &str) -> Result<String, ConfigError> {
let content = std::fs::read_to_string(path)?; // io::Error → ConfigError::FileRead
if content.is_empty() {
return Err(ConfigError::Invalid {
message: "config file is empty".to_string(),
});
}
Ok(content)
}
}
🟢 Self-study checkpoint: Before continuing, make sure you can answer:
🟢 自测检查点: 继续往下之前,先确认下面两个问题能答上来:
- Why does
?on theread_to_stringcall work? (Because#[from]generatesimpl From<io::Error> for ConfigError.)
1. 为什么read_to_string后面的?能直接工作?因为#[from]会生成impl From<io::Error> for ConfigError。- What happens if you add a third variant
MissingKey(String)— what code changes? (Usually just add the variant; existing code still compiles.)
2. 如果再加一个MissingKey(String)变体,需要改什么?通常只要把变体加上,已有代码还是能继续编译。
Crate-Level Error Types and Result Aliases
crate 级错误类型与 Result 别名
As the project grows beyond a single file, multiple module-level errors usually need to be merged into a crate-level error type. This is the standard production pattern in Rust.
项目一旦超过单文件玩具规模,就会出现多个模块各自报错的情况。这时通常要把它们并进一个 crate 级错误类型 里,这就是生产代码里最常见的写法。
In real-world Rust projects, every crate or major module often defines its own Error enum and a Result type alias. This is idiomatic Rust, and in spirit it resembles defining a per-library exception hierarchy plus using Result = std::expected<T, Error> in modern C++.
现实里的 Rust 项目通常会给每个 crate,或者至少每个重要模块,定义自己的 Error 枚举,再顺手配一个 Result 类型别名。这就是惯用法。类比到现代 C++,差不多就是给每个库准备一套异常层级,再写一个 using Result = std::expected<T, Error>。
The pattern
基本模式
#![allow(unused)]
fn main() {
// src/error.rs (or at the top of lib.rs)
use thiserror::Error;
/// Every error this crate can produce.
#[derive(Error, Debug)]
pub enum Error {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error), // auto-converts via From
#[error("JSON parse error: {0}")]
Json(#[from] serde_json::Error), // auto-converts via From
#[error("Invalid sensor id: {0}")]
InvalidSensor(u32), // domain-specific variant
#[error("Timeout after {ms} ms")]
Timeout { ms: u64 },
}
/// Crate-wide Result alias — saves typing throughout the crate.
pub type Result<T> = core::result::Result<T, Error>;
}
How it simplifies every function
它如何让每个函数都清爽很多
Without the alias, every signature needs to repeat the full error type:
没有别名时,每个函数签名都得重复一遍完整错误类型:
#![allow(unused)]
fn main() {
// Verbose — error type repeated everywhere
fn read_sensor(id: u32) -> Result<f64, crate::Error> { ... }
fn parse_config(path: &str) -> Result<Config, crate::Error> { ... }
}
With the alias, the signatures become much cleaner:
有了别名以后,签名立刻干净一大截:
#![allow(unused)]
fn main() {
// Clean — just `Result<T>`
use crate::{Error, Result};
fn read_sensor(id: u32) -> Result<f64> {
if id > 128 {
return Err(Error::InvalidSensor(id));
}
let raw = std::fs::read_to_string(format!("/dev/sensor/{id}"))?; // io::Error → Error::Io
let value: f64 = raw.trim().parse()
.map_err(|_| Error::InvalidSensor(id))?;
Ok(value)
}
}
The #[from] attribute on Io generates the following impl automatically:Io 变体上的 #[from] 会自动生成下面这样的 impl:
#![allow(unused)]
fn main() {
// Auto-generated by thiserror's #[from]
impl From<std::io::Error> for Error {
fn from(source: std::io::Error) -> Self {
Error::Io(source)
}
}
}
That is why ? works. When the inner call returns std::io::Error but the outer function returns Result<T> using the alias, the compiler inserts From::from() and converts the error automatically.
这就是 ? 能工作的根本原因。内层返回 std::io::Error,外层函数返回的是别名 Result<T>,编译器会在中间自动插入 From::from() 完成转换。
Composing module-level errors
把模块级错误拼成 crate 级错误
Larger crates often define errors per module and compose them at the crate root:
规模再大一点的 crate,通常会让每个模块先定义自己的错误,然后在 crate 根部统一汇总:
#![allow(unused)]
fn main() {
// src/config/error.rs
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
#[error("Missing key: {0}")]
MissingKey(String),
#[error("Invalid value for '{key}': {reason}")]
InvalidValue { key: String, reason: String },
}
// src/error.rs (crate-level)
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)] // delegates Display to inner error
Config(#[from] crate::config::ConfigError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = core::result::Result<T, Error>;
}
Callers can still match on specific configuration errors:
即便统一到了 crate 级错误,调用者依然可以继续匹配具体的配置错误:
#![allow(unused)]
fn main() {
match result {
Err(Error::Config(ConfigError::MissingKey(k))) => eprintln!("Add '{k}' to config"),
Err(e) => eprintln!("Other error: {e}"),
Ok(v) => use_value(v),
}
}
C++ comparison
和 C++ 的对照
| Concept 概念 | C++ | Rust |
|---|---|---|
| Error hierarchy 错误层级 | class AppError : public std::runtime_error | #[derive(thiserror::Error)] enum Error { ... } |
| Return error 返回错误 | std::expected<T, Error> or throw | fn foo() -> Result<T> |
| Convert error 错误转换 | Manual try/catch + rethrow手写 try/catch 再重新抛出 | #[from] + ? — zero boilerplate#[from] 配合 ?,几乎不用样板代码 |
Result aliasResult 别名 | template<class T> using Result = std::expected<T, Error>; | pub type Result<T> = core::result::Result<T, Error>; |
| Error message 错误消息 | Override what()重写 what() | #[error("...")] — compiled into Display impl#[error("...")] 会生成 Display 实现 |