当前位置: 首页 > news >正文

Rust 的错误处理:别拿类型系统当护身符

文章目录

    • 一、错误的核心:调用者到底需要知道什么?
      • 错误示例:不区分错误来源
      • 正确示例:枚举错误来源
    • 二、错误信息越多不代表更好
      • 过度设计的灾难
      • 实用设计:擦除无关信息
    • 三、特殊情况是设计的失败
      • 错误示例:用 Option 偷懒
      • 正确示例:用 Result 明确语义
    • 四、操作符不是魔法,是糖衣
      • 错误示例:只实现了 Into
      • 正确示例:实现 From,一切通顺
    • 五、try block:清理不该被跳过的东西
      • 正确做法:try block
    • 六、永远别为“优雅”破坏用户空间

大多数 Rust 程序员在处理错误时表现得像被编译器吓坏了的孩子。
他们害怕 Result、害怕 ?、害怕 lifetimes,最后写出一堆看起来“类型安全”的垃圾。
问题不在语言,而在思维:错误处理是设计问题,不是语法问题。


一、错误的核心:调用者到底需要知道什么?

如果调用者需要知道错误的来源,就枚举(enumeration)。
如果调用者只需要知道“出错了”,就擦除(erasure)。


错误示例:不区分错误来源

fn copy_data(mut reader: impl Read, mut writer: impl Write) -> Result<(), std::io::Error> {std::io::copy(&mut reader, &mut writer)?;Ok(())
}

看起来很简洁对吧?问题是:调用者根本不知道是读挂了还是写炸了。

在网络服务器里,这两个错误是完全不同的:

  • 输入流失败可能意味着磁盘或 socket 损坏(致命)
  • 输出流失败可能只是客户端断开(可以忽略)

但现在它们都被混在一个 std::io::Error 里,你的调用者只能瞎猜。


正确示例:枚举错误来源

#[derive(Debug)]
pub enum CopyError {In(std::io::Error),Out(std::io::Error),
}impl std::error::Error for CopyError {}
impl std::fmt::Display for CopyError {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {match self {CopyError::In(e) => write!(f, "input error: {}", e),CopyError::Out(e) => write!(f, "output error: {}", e),}}
}fn copy_data(mut reader: impl Read, mut writer: impl Write) -> Result<(), CopyError> {let mut buf = [0; 4096];loop {let n = reader.read(&mut buf).map_err(CopyError::In)?;if n == 0 { break; }writer.write_all(&buf[..n]).map_err(CopyError::Out)?;}Ok(())
}

现在调用者能区分错误来源,可以决定不同的策略。
这才叫语义清晰。类型系统不是目标,它是防止你撒谎的手段。


二、错误信息越多不代表更好

有的人以为“详细 = 专业”,于是他们搞出这种 monstrosity:

过度设计的灾难

#[derive(Debug)]
enum DecodeError {InvalidHeader(u32),UnsupportedCompression(String),MalformedChunk(usize),IoError(std::io::Error),
}

然后每个错误都被上报、打印、打 tag。
问题是:上层调用者根本不在意。
他们只想知道“图片读不出来”,不在乎是哪个 bit 出问题。


实用设计:擦除无关信息

#[derive(Debug)]
struct ImageError(String);impl std::fmt::Display for ImageError {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {write!(f, "image decoding failed: {}", self.0)}
}impl std::error::Error for ImageError {}fn decode_image(data: &[u8]) -> Result<Image, ImageError> {// 内部可以区分多种错误let header = parse_header(data).map_err(|_| ImageError("invalid header".into()))?;decompress(header).map_err(|_| ImageError("decompression failed".into()))?;Ok(Image::new())
}

用户得到的是干净的 ImageError,无需知道底层细节。
擦除复杂度,不是隐藏错误,而是隔离不必要的信息。


三、特殊情况是设计的失败

很多人写函数时遇到“理论上不会失败”的情况,就乱来。

错误示例:用 Option 偷懒

fn parse_number(s: &str) -> Option<i32> {s.parse().ok()
}

看起来简洁,但 None 到底是什么意思?
是字符串不是数字?是空字符串?是 I/O 出错?
调用者完全没法判断——你直接剥夺了他们的恢复能力。


正确示例:用 Result 明确语义

#[derive(Debug)]
struct ParseNumberError;impl std::fmt::Display for ParseNumberError {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {write!(f, "invalid number format")}
}impl std::error::Error for ParseNumberError {}fn parse_number(s: &str) -> Result<i32, ParseNumberError> {s.parse().map_err(|_| ParseNumberError)
}

Result 表示“有错误发生”;Option 表示“没有值返回”。
混用这两个类型,是 API 设计的懒惰行为。
如果函数可能失败,就让类型系统告诉调用者这件事。


四、操作符不是魔法,是糖衣

有人以为 ? 很神秘,其实它就是:

match expr {Ok(v) => v,Err(e) => return Err(e.into()),
}

所以关键不在 ?,而在 From trait


错误示例:只实现了 Into

impl Into<MyError> for std::io::Error {fn into(self) -> MyError { MyError::Io(self) }
}

然后惊讶地发现:? 报错,“trait bound not satisfied”。
因为 ? 调用的是 From::from,不是 Into::into


正确示例:实现 From,一切通顺

impl From<std::io::Error> for MyError {fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}

从此你可以愉快地写:

fn read_file() -> Result<String, MyError> {let mut buf = String::new();std::fs::File::open("config.txt")?.read_to_string(&mut buf)?;Ok(buf)
}

五、try block:清理不该被跳过的东西

很多人喜欢:

fn run() -> Result<(), Error> {let conn = connect()?;do_stuff(&conn)?;conn.close()?; // 这一行永远执行不到Ok(())
}

一旦 do_stuff 出错,close() 永远不会跑到。
Rust 的 ? 让你早退,但不会帮你擦屁股。


正确做法:try block

fn run() -> Result<(), Error> {let conn = connect()?;let r = try {do_stuff(&conn)?;};conn.close()?;r
}

这才是可靠的错误处理:不丢资源,不绕逻辑
try {} 块让你能在出错时执行 cleanup,而不破坏 ? 的流畅性。


六、永远别为“优雅”破坏用户空间

有些库作者干了这种蠢事:

// v1.0
pub fn foo() -> Result<(), Box<dyn Error>>;// v1.1
pub fn foo() -> Result<(), Box<MyError>>;

然后他们说:“签名没变呀!编译器不会报错!”

但用户代码里:

match foo() {Err(e) => {if let Some(ioe) = e.downcast_ref::<std::io::Error>() {// ...}}
}

现在全部崩了。
如果用户能 downcast,那类型擦除就是你的 API 一部分。
别假装不是。
这类破坏兼容性的更改,永远是破坏性更新(breaking change)。


http://www.dtcms.com/a/491482.html

相关文章:

  • 用栈实现记忆存储——C++语言自制时间计算器
  • 实验二 呼吸灯功能实验
  • 动力 网站建设珠海专业网站建设费用
  • 博客系统测试
  • 高德地图电子围栏/地图选区/地图打点
  • 自己动手建设网站过程dede珠宝商城网站源码
  • Git的分支
  • 基础拓展
  • 手机微网站建设河南网站建设的详细策划
  • 剧本杀小程序系统开发:内容生态与商业模式的双轮驱动
  • 网站备案表不会写引流网站怎么做
  • 【系统分析师】写作框架:数据灾务技术与应用
  • 香港云服务器域名无法访问的原因
  • 荆门网站建设服务上海网站制作网络推广方法
  • 系统那个网站好什么是网站平台开发
  • 网站管理后台地址深圳做网站排名
  • 软件测试及 AI+测试
  • oj字符串,求助讨论帖
  • 鸿蒙app开发中 class类中的 访问修饰符和静态修饰符 等这些命名的含义 以及用法
  • 大模型-AIGC技术在文本生成与音频生成领域的应用
  • 国内产品网站1688利用腾讯云建设网站
  • 下载免费网站模板下载移动网站建设机构
  • 公司网站怎么建温泉网站建设
  • error: can‘t find Rust compiler
  • 关于力扣第 167 场双周赛的赛后总结 第三四题
  • 网站开发h5技术两学一做网站源码
  • SpringBoot-自动配置原理
  • AI应用开发001-Conda和Jupyter Lab介绍
  • 在 Android Shell 终端上直接运行 OPENPPP2 网关路由配置指南
  • debug - MDK - arm-none-eabi - 从MDK工程做一个makefile工程出来