穷尽性模式匹配:编译器保证 vs 运行时错误
你将学到: 为什么 C# 的
switch表达式会默不作声地遗漏某些情况,而 Rust 的match则会在编译期捕捉到这些遗漏;Option<T>与Nullable<T>在空安全(null safety)方面的对比;以及如何使用Result<T, E>自定义错误类型。难度: 🟡 中级
C# Switch 表达式 — 依然不够完整
// C# switch 表达式看起来是穷尽的,但其实并没有强力保证
public enum HttpStatus { Ok, NotFound, ServerError, Unauthorized }
public string HandleResponse(HttpStatus status) => status switch
{
HttpStatus.Ok => "Success",
HttpStatus.NotFound => "Resource not found",
HttpStatus.ServerError => "Internal error",
// 遗漏了 Unauthorized 情况 —— 编译器仅给出警告 CS8524,而不是错误!
// 运行时:如果 status 是 Unauthorized,会抛出 SwitchExpressionException
};
// 即便开启了可空引用类型警告,下面这段代码依然能通过编译:
public string ProcessUser(User? user) => user switch
{
{ IsActive: true } => $"Active: {user.Name}",
{ IsActive: false } => $"Inactive: {user.Name}",
// 遗漏了 null 的情况 —— 编译器警告 CS8655,但依然不是错误!
// 运行时:当 user 为 null 时抛出 SwitchExpressionException
};
Rust 模式匹配 — 真正的穷尽性
#![allow(unused)]
fn main() {
#[derive(Debug)]
enum HttpStatus {
Ok,
NotFound,
ServerError,
Unauthorized,
}
fn handle_response(status: HttpStatus) -> &'static str {
match status {
HttpStatus::Ok => "Success",
HttpStatus::NotFound => "Resource not found",
HttpStatus::ServerError => "Internal error",
HttpStatus::Unauthorized => "Authentication required",
// 如果漏掉任何一种情况,都会发生【编译错误】!
// 这段代码根本无法生成可执行文件
}
}
// 稍后添加新的枚举变体,会使所有现存的 match 语句编译失败
#[derive(Debug)]
enum HttpStatus {
// ... 原有变体 ...
Forbidden, // 添加这一行会使 handle_response() 编译报错
}
// 编译器会强制你处理所有(包含新增的)情况
}
graph TD
subgraph "C# 模式匹配的局限性"
CS_SWITCH["switch 表达式"]
CS_WARNING["⚠️ 仅提供编译器警告"]
CS_COMPILE["✅ 依然能编译成功"]
CS_RUNTIME["💥 导致运行时异常"]
CS_DEPLOY["❌ Bug 流入生产环境"]
CS_SILENT["😰 枚举变更时的哑失效率"]
CS_SWITCH --> CS_WARNING
CS_WARNING --> CS_COMPILE
CS_COMPILE --> CS_RUNTIME
CS_RUNTIME --> CS_DEPLOY
CS_SWITCH --> CS_SILENT
end
subgraph "Rust 穷尽性匹配"
RUST_MATCH["match 表达式"]
RUST_ERROR["🛑 编译失败"]
RUST_FIX["✅ 必须处理所有情况"]
RUST_SAFE["✅ 零运行时意外状况"]
RUST_EVOLUTION["🔄 枚举变更会导致编译中断"]
RUST_REFACTOR["🛠️ 强制性重构保证安全"]
RUST_MATCH --> RUST_ERROR
RUST_ERROR --> RUST_FIX
RUST_FIX --> RUST_SAFE
RUST_MATCH --> RUST_EVOLUTION
RUST_EVOLUTION --> RUST_REFACTOR
end
style CS_RUNTIME fill:#ffcdd2,color:#000
style CS_DEPLOY fill:#ffcdd2,color:#000
style CS_SILENT fill:#ffcdd2,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
style RUST_REFACTOR fill:#c8e6c9,color:#000
空安全:Nullable<T> vs Option<T>
Rust 的 Option<T> 系统
#![allow(unused)]
fn main() {
// Rust - 利用 Option<T> 进行显式的空值处理
#[derive(Debug)]
pub struct User {
name: String, // 绝不会为 null
email: Option<String>, // 显式的可选字段
}
impl User {
pub fn get_display_name(&self) -> &str {
&self.name // 无需空检查 —— 保证存在
}
pub fn get_email_or_default(&self) -> String {
self.email
.as_ref()
.map(|e| e.clone())
.unwrap_or_else(|| "[email protected]".to_string())
}
}
}
graph TD
subgraph "C# 空值处理演进"
CS_NULL["传统方式:string name<br/>[错误] 可能会是 null"]
CS_NULLABLE["Nullable<T>:int? value<br/>[OK] 值类型有了显式空值"]
CS_NRT["可空引用类型 (NRT)<br/>string? name<br/>[警告] 仅提供编译期警告"]
CS_RUNTIME["运行时 NullReferenceException<br/>[错误] 依然可能崩溃"]
CS_NULL --> CS_RUNTIME
CS_NRT -.-> CS_RUNTIME
end
subgraph "Rust Option<T> 系统"
RUST_OPTION["Option<T><br/>Some(值) | None"]
RUST_FORCE["编译器强制要求处理<br/>[OK] 无法忽略 None"]
RUST_MATCH["模式匹配<br/>match option { ... }"]
RUST_METHODS["丰富的 API<br/>.map(), .unwrap_or(), .and_then()"]
RUST_OPTION --> RUST_FORCE
RUST_FORCE --> RUST_MATCH
RUST_FORCE --> RUST_METHODS
RUST_SAFE["编译期空安全<br/>[OK] 无空指针异常"]
RUST_MATCH --> RUST_SAFE
RUST_METHODS --> RUST_SAFE
end
style CS_RUNTIME fill:#ffcdd2,color:#000
style RUST_SAFE fill:#c8e6c9,color:#000
style CS_NRT fill:#fff3e0,color:#000
错误处理:Option 与 Result 类型
use std::collections::HashMap;
struct PersonService {
people: HashMap<i32, String>,
}
impl PersonService {
// 返回 Option<T> 而不是 null!
fn find_person(&self, id: i32) -> Option<&String> {
self.people.get(&id)
}
// 使用 Result<T, E> 进行错误处理
fn save_person(&mut self, id: i32, name: String) -> Result<(), String> {
if name.is_empty() {
return Err("名称不能为空".to_string());
}
self.people.insert(id, name);
Ok(())
}
}
fn main() {
let mut service = PersonService { people: HashMap::new() };
// 问号运算符 (Question mark operator) 用于早期返回
fn try_operation(service: &mut PersonService) -> Result<String, String> {
service.save_person(2, "Bob".to_string())?; // 若发生错误则直接返回
let name = service.find_person(2).ok_or("未找到人员")?; // 将 Option 转换为 Result
Ok(format!("Hello, {}", name))
}
}
自定义错误类型
#![allow(unused)]
fn main() {
// 定义自定义错误枚举
#[derive(Debug)]
enum PersonError {
NotFound(i32),
InvalidName(String),
DatabaseError(String),
}
impl std::fmt::Display for PersonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PersonError::NotFound(id) => write!(f, "未找到 ID 为 {} 的人员", id),
PersonError::InvalidName(name) => write!(f, "无效的名称: '{}'", name),
PersonError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
}
}
}
impl std::error::Error for PersonError {}
}
练习
🏋️ 练习:Option 组合算子 (Combinators) (点击展开)
使用 Rust 的 Option 组合算子(and_then、map、unwrap_or)重写下面这段深层嵌套的 C# 空值检查代码:
string GetCityName(User? user)
{
if (user != null)
if (user.Address != null)
if (user.Address.City != null)
return user.Address.City.ToUpper();
return "UNKNOWN";
}
使用以下 Rust 类型:
#![allow(unused)]
fn main() {
struct User { address: Option<Address> }
struct Address { city: Option<String> }
}
请将其写成一个 单一表达式,且不使用 if let 或 match。
🔑 参考答案
struct User { address: Option<Address> }
struct Address { city: Option<String> }
fn get_city_name(user: Option<&User>) -> String {
user.and_then(|u| u.address.as_ref())
.and_then(|a| a.city.as_ref())
.map(|c| c.to_uppercase())
.unwrap_or_else(|| "UNKNOWN".to_string())
}
fn main() {
let user = User {
address: Some(Address { city: Some("seattle".to_string()) }),
};
assert_eq!(get_city_name(Some(&user)), "SEATTLE");
assert_eq!(get_city_name(None), "UNKNOWN");
}
核心洞见:对于 Option 来说,and_then 就像 Rust 版本的 ?. 运算符。每一步都会返回 Option,链条会在遇到第一个 None 时短路 —— 这与 C# 的空条件运算符 ?. 逻辑完全一致,但更加显式且类型安全。