Exceptions vs Result<T, E> | 异常 vs Result<T, E>
What you’ll learn: Why Rust replaces exceptions with
Result<T, E>andOption<T>, the?operator for concise error propagation, and how explicit error handling eliminates hidden control flow that plagues C#try/catchcode.你将学到什么: 为什么 Rust 用
Result<T, E>和Option<T>替代异常, 如何使用?操作符进行简洁的错误传播,以及显式错误处理如何消除 C#try/catch代码中常见的隐藏控制流问题。Difficulty: Intermediate
难度: 中级
See also: Crate-Level Error Types for production error patterns with
thiserrorandanyhow, and Essential Crates for the error crate ecosystem.另见: Crate 级错误类型 了解基于
thiserror和anyhow的生产级错误处理模式;常用 Crate 则介绍错误处理相关生态。
C# Exception-Based Error Handling | C# 基于异常的错误处理
// C# - Exception-based error handling
public class UserService
{
public User GetUser(int userId)
{
if (userId <= 0)
{
throw new ArgumentException("User ID must be positive");
}
var user = database.FindUser(userId);
if (user == null)
{
throw new UserNotFoundException($"User {userId} not found");
}
return user;
}
public async Task<string> GetUserEmailAsync(int userId)
{
try
{
var user = GetUser(userId);
return user.Email ?? throw new InvalidOperationException("User has no email");
}
catch (UserNotFoundException ex)
{
logger.Warning("User not found: {UserId}", userId);
return "noreply@company.com";
}
catch (Exception ex)
{
logger.Error(ex, "Unexpected error getting user email");
throw; // Re-throw
}
}
}
Rust Result-Based Error Handling | Rust 基于 Result 的错误处理
#![allow(unused)]
fn main() {
use std::fmt;
#[derive(Debug)]
pub enum UserError {
InvalidId(i32),
NotFound(i32),
NoEmail,
DatabaseError(String),
}
impl fmt::Display for UserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UserError::InvalidId(id) => write!(f, "Invalid user ID: {}", id),
UserError::NotFound(id) => write!(f, "User {} not found", id),
UserError::NoEmail => write!(f, "User has no email address"),
UserError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
}
}
}
impl std::error::Error for UserError {}
pub struct UserService {
// database connection, etc.
}
impl UserService {
pub fn get_user(&self, user_id: i32) -> Result<User, UserError> {
if user_id <= 0 {
return Err(UserError::InvalidId(user_id));
}
// Simulate database lookup
self.database_find_user(user_id)
.ok_or(UserError::NotFound(user_id))
}
pub fn get_user_email(&self, user_id: i32) -> Result<String, UserError> {
let user = self.get_user(user_id)?; // ? operator propagates errors
user.email
.ok_or(UserError::NoEmail)
}
pub fn get_user_email_or_default(&self, user_id: i32) -> String {
match self.get_user_email(user_id) {
Ok(email) => email,
Err(UserError::NotFound(_)) => {
log::warn!("User not found: {}", user_id);
"noreply@company.com".to_string()
}
Err(err) => {
log::error!("Error getting user email: {}", err);
"error@company.com".to_string()
}
}
}
}
}
graph TD
subgraph "C# Exception Model"
CS_CALL["Method Call"]
CS_SUCCESS["Success Path"]
CS_EXCEPTION["throw Exception"]
CS_STACK["Stack unwinding<br/>(Runtime cost)"]
CS_CATCH["try/catch block"]
CS_HIDDEN["[ERROR] Hidden control flow<br/>[ERROR] Performance cost<br/>[ERROR] Easy to ignore"]
CS_CALL --> CS_SUCCESS
CS_CALL --> CS_EXCEPTION
CS_EXCEPTION --> CS_STACK
CS_STACK --> CS_CATCH
CS_EXCEPTION --> CS_HIDDEN
end
subgraph "Rust Result Model"
RUST_CALL["Function Call"]
RUST_OK["Ok(value)"]
RUST_ERR["Err(error)"]
RUST_MATCH["match result"]
RUST_QUESTION["? operator<br/>(early return)"]
RUST_EXPLICIT["[OK] Explicit error handling<br/>[OK] Zero runtime cost<br/>[OK] Cannot ignore errors"]
RUST_CALL --> RUST_OK
RUST_CALL --> RUST_ERR
RUST_OK --> RUST_MATCH
RUST_ERR --> RUST_MATCH
RUST_ERR --> RUST_QUESTION
RUST_MATCH --> RUST_EXPLICIT
RUST_QUESTION --> RUST_EXPLICIT
end
style CS_HIDDEN fill:#ffcdd2,color:#000
style RUST_EXPLICIT fill:#c8e6c9,color:#000
style CS_STACK fill:#fff3e0,color:#000
style RUST_QUESTION fill:#c8e6c9,color:#000
The ? Operator: Propagating Errors Concisely | ? 操作符:简洁地传播错误
// C# - Exception propagation (implicit)
public async Task<string> ProcessFileAsync(string path)
{
var content = await File.ReadAllTextAsync(path); // Throws on error
var processed = ProcessContent(content); // Throws on error
return processed;
}
#![allow(unused)]
fn main() {
// Rust - Error propagation with ?
fn process_file(path: &str) -> Result<String, ConfigError> {
let content = read_config(path)?; // ? propagates error if Err
let processed = process_content(&content)?; // ? propagates error if Err
Ok(processed) // Wrap success value in Ok
}
fn process_content(content: &str) -> Result<String, ConfigError> {
if content.is_empty() {
Err(ConfigError::InvalidFormat)
} else {
Ok(content.to_uppercase())
}
}
}
Option<T> for Nullable Values | 用 Option<T> 表示可空值
// C# - Nullable reference types
public string? FindUserName(int userId)
{
var user = database.FindUser(userId);
return user?.Name; // Returns null if user not found
}
public void ProcessUser(int userId)
{
string? name = FindUserName(userId);
if (name != null)
{
Console.WriteLine($"User: {name}");
}
else
{
Console.WriteLine("User not found");
}
}
#![allow(unused)]
fn main() {
// Rust - Option<T> for optional values
fn find_user_name(user_id: u32) -> Option<String> {
// Simulate database lookup
if user_id == 1 {
Some("Alice".to_string())
} else {
None
}
}
fn process_user(user_id: u32) {
match find_user_name(user_id) {
Some(name) => println!("User: {}", name),
None => println!("User not found"),
}
// Or use if let (pattern matching shorthand)
if let Some(name) = find_user_name(user_id) {
println!("User: {}", name);
} else {
println!("User not found");
}
}
}
Combining Option and Result | 组合使用 Option 与 Result
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b != 0.0 {
Some(a / b)
} else {
None
}
}
fn parse_and_divide(a_str: &str, b_str: &str) -> Result<Option<f64>, ParseFloatError> {
let a: f64 = a_str.parse()?; // Return parse error if invalid
let b: f64 = b_str.parse()?; // Return parse error if invalid
Ok(safe_divide(a, b)) // Return Ok(Some(result)) or Ok(None)
}
use std::num::ParseFloatError;
fn main() {
match parse_and_divide("10.0", "2.0") {
Ok(Some(result)) => println!("Result: {}", result),
Ok(None) => println!("Division by zero"),
Err(error) => println!("Parse error: {}", error),
}
}
Rust 的关键点不是“没有异常”,而是成功与失败都被写进了类型系统,调用者必须正面处理它。
Exercise: Build a Crate-Level Error Type | 练习:构建 Crate 级错误类型 (click to expand / 点击展开)
Challenge: Create an AppError enum for a file processing application that can fail due to I/O errors, JSON parse errors, and validation errors. Implement From conversions for automatic ? propagation.
挑战: 为一个文件处理应用创建 AppError 枚举。它可能因为 I/O 错误、JSON 解析错误和校验错误而失败。请实现 From 转换,使 ? 能自动传播这些错误。
#![allow(unused)]
fn main() {
// Starter code
use std::io;
// TODO: Define AppError with variants:
// Io(io::Error), Json(serde_json::Error), Validation(String)
// TODO: Implement Display and Error traits
// TODO: Implement From<io::Error> and From<serde_json::Error>
// TODO: Define type alias: type Result<T> = std::result::Result<T, AppError>;
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?; // io::Error -> AppError
let config: Config = serde_json::from_str(&content)?; // serde error -> AppError
if config.name.is_empty() {
return Err(AppError::Validation("name cannot be empty".into()));
}
Ok(config)
}
}
Solution | 参考答案
#![allow(unused)]
fn main() {
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Validation: {0}")]
Validation(String),
}
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(serde::Deserialize)]
struct Config {
name: String,
port: u16,
}
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)?;
let config: Config = serde_json::from_str(&content)?;
if config.name.is_empty() {
return Err(AppError::Validation("name cannot be empty".into()));
}
Ok(config)
}
}
Key takeaways:
thiserrorgeneratesDisplayandErrorimpls from attributes#[from]generatesFrom<T>impls, enabling automatic?conversion- The
Result<T>alias eliminates boilerplate throughout your crate - Unlike C# exceptions, the error type is visible in every function signature
关键要点:
thiserror可以通过属性自动生成Display和Error实现#[from]会生成From<T>实现,从而支持?自动转换错误Result<T>类型别名可以减少整个 crate 中的样板代码- 和 C# 的异常不同,Rust 会在每个函数签名中显式暴露错误类型