Rust中错误处理最佳实践:抛弃臃肿枚举!


(撸起袖子)来来来,老师给你们翻译翻译什么叫错误处理!现在Rust圈子里流行这么个玩法:

【错误处理の校园版】
想象你开了一家奶茶店(crate),现在要给每种奶茶(模块)写一份《可能翻车清单》(错误枚举)。比如珍珠奶茶可能"珍珠煮糊了"、"糖加多了",水果茶可能"水果不新鲜"、"冰块不够"。有些店更狠,直接写一本《本店所有翻车大全》(全局错误枚举)。

但这样很蠢对吧?比如你做水果茶的服务员明明不会煮珍珠,但《翻车清单》里还是列着"珍珠煮糊了"这一条。顾客(调用代码)得自己翻说明书(文档)才知道哪些错误根本不会发生——谁特么看说明书啊!(摔)

【痛点暴击】
Rust最牛逼的地方不就是用类型系统当保安,不让代码干坏事吗?结果现在大家集体摆烂,整出一堆《新华字典》那么厚的错误枚举。当然也能理解——要是给每个函数都单独写错误类型,还要搞类型转换,那得累死程序猿啊!所以现在99%的人都选择躺平用大杂烩错误类型...除了几个头铁的学霸还在坚持。

【叛逆方案】
其实错误就应该像微信消息一样,一条是一条!函数只是可能返回某些错误组合,而不是定义错误。最早这么玩的是"恐怖"库(快去围观!),虽然要写很多.map_err(OneOf::broaden)这种咒语,错误多的时候还得手动拼乐高,但人家设计是真的优雅啊...(远目)

【老师的私藏神器】
最近发现个叫error_set!的宏,简直是类型体操外挂!看好了:

rust
errorset! {
    奶茶机故障 = { 机器冒烟(btleplug::Error) };  // 引用其他错误类型
    
    找奶茶杯错误 = { 没吸管, 超时, 杯子失踪 } || 奶茶机故障 || 过滤奶茶错误;
    过滤奶茶错误 = { 连接断开, 超时 } || 奶茶机故障;
    
    // 自动实现错误类型转换
    fn 做奶茶() -> Result<(), 找奶茶杯错误> {
        找杯子()?;  // 这里?自动转换错误类型
        Ok(())
    }
}

这个宏可以:
1. 用"||"符号组合错误类型,像拼积木一样
2. 自动生成转换逻辑,?操作符直接起飞
3. 甚至支持带字段的错误结构体

Rust代码:

error_set! {
    BtlePlug = {
        BtlePlug(btleplug::Error)
    };

    FindSDeviceError = { BLENoAdapter, Timeout, NoSDevice } || BtlePlug || FilterSDeviceError;
    FilterSDeviceError = { BLEAdapterDisconnect, Timeout } || BtlePlug;
    ConnectToSDeviceError = {NoRxChar, NoTxChar, NoKaChar} || BtlePlug;
    ConnectAndRunError = FindSDeviceError
                        || ConnectToSDeviceError
                        || ForwardToMainThreadError
                        || ForwardToSDeviceError;
                        || BtlePlug
    ForwardToSDeviceError = {MainThreadDied, } || BtlePlug;
    ForwardToMainThreadError = {SendError(mpsc::SendError<Vec<u8>>)};

    DecoderError = {Invalid,};

    #[derive(PartialEq)]
    CrcError = { CrcMissmatch {actual: u16, expected: u16}, ConversionError };
}

它允许我们从变量和与其他错误集的联合中创建错误集。如果你使用的错误集是函数错误集的子集,那么?操作符将起作用,即使你不使用union操作符,它也会发现是否是这种情况,即:

error_set! {
    A = { Foo, Bar, Baz };
    B = { Foo, Bar };
}

fn b() -> Result<(), B> {
    Err(B::Foo)
}

fn a() -> Result<(), A> {
    b()?;
    Ok(())
}


虽然处理复杂错误时还是要写点模板代码,但比原来轻松多了!还有像例如SmartErr之类的库也在探索这个方向,最夸张的甚至有个#[属性宏]能自动分析函数体生成错误类型...(虽然老师现在找不到这个库了,知道的同学私我啊!)

(敲黑板)总之:错误处理应该像发朋友圈一样简单精准,而不是让所有人交同一份检讨书!