语言优秀的错误处理能力,会大大减少错误对整体流程的破坏,减少我们码农的心智负担。
我们一般处理错误的流程:
主要有三种方法:
一、使用返回值(错误码)有很多例子 比如:
在 C 语言中,如果 fopen(Filename) 无法打开文件,会返回 NULL,调用者通过判断返回值是否为 NULL,来进行相应的错误处理。
这样有很多局限,返回值本来有自己的语义,非要把错误和返回值混淆在一起,加重了开发者的心智负担。
Golang对返回值做了扩展,可以返回多个参数,可以返回专门的error类型。
funcFread(file*File,b[]byte)(nint,errerror)
这样就可以把错误和正常的返回区分开来了。 这样一来这个err就会在调用链中显式传播。 所以在Golang的代码中随处可见的
ifErr!=nil{
//错误处理……
}
二、使用异常
由于返回值不利于错误的传播,Java等语言使用异常来处理错误。
异常可以看成关注点分离:错误的产生和处理是分隔开的,调用者不必关心错误。
程序中任何可能出错的地方,都可以抛出异常;而异常可以通过栈回溯(stack unwind)被一层层自动传递,直到遇到捕获异常的地方,如果回溯到 main 函数还无人捕获,程序就会崩溃。如下图所示:
这样可以简化错误处理流程,解决了返回值传播的问题。
用异常更容易写代码,但当异常安全无法保证时,程序的正确性会受到很大的挑战。 可是保证异常安全的第一个原则就是:避免抛出异常。
异常处理另外一个比较严重的问题是:开发者会滥用异常。异常处理的开销要比处理返回值大得多,滥用会有很多额外的开销。
三、使用类型系统错误信息既然可以通过已有的类型携带,或者通过多返回值的方式提供,那通过类型来表征错误,用一个内部包含正常返回类型和错误返回类型的复合类型,通过类型系统来强制错误的处理和传递效果更好。(Golang 好像就是这样)
但我们前面提到用返回值返回错误的缺点:错误需要被调用者立即处理,或显式传递。 用类型来处理错误的好处是:可以用函数式编程,简化错误的处理。 如:map、fold等函数,让代码不那么冗余。
Rust错误处理Rust总结前辈的经验,使用类型系统来构建主要的错误处理流程。 构建了Option类型和Result类型。 代码定义如下:
pubenumOption<T>{
None,
Some(T),
}
#[must_use="this`Result`maybean`Err`variant,whichshouldbehandled"]
pubenumResult<T,E>{
Ok(T),
Err(E),
}
可以看到Result类型有must_use, 如果没有使用就会报warning,以保证错误被处理了。
上图中的例子,如果我们不处理read_file的返回值,就开始有提示了。 (那这不是回到了 Golang的 到处都是 if err != nil的情况了吗?)
?操作符如果执行传播错误,不想当时处理,就用?操作符。这样让错误传播和异常处理不相上下,同时又避免了异常处理带来的问题。
usestd::fs::File;
usestd::io::Read;
fnread_file(name:&str)->Result<String,std::io::Error>{
letmutf=File::open(name)?;
letmutcontents=String::new();
f.read_to_string(&mutcontents)?;
Ok(contents)
}
?操作符 展开来就类似这样:
matchresult{
Ok(v)=>v,
Err(e)=>returnErr(e.into())
}
我们就能写出这样的函数式编程的代码。
fut
.await?
.process()?
.next()
.await?;
流程如图:
注意: 在不同错误类型之间是无法直接使用的,需要实现From trait在二者之间建立转换桥梁。
Error trait 和错误类型的转换Result<T, E> 里 E 是一个代表错误的数据类型。 为了规范这个代表错误的数据类型的行为,Rust 定义了 Error trait:
pubtraitError:Debug Display{
fnsource(&self)->Option<&(dynError 'static)>{...}
fnbacktrace(&self)->Option<&Backtrace>{...}
fndescription(&self)->&str{...}
fncause(&self)->Option<&dynError>{...}
}
也可以自定义数据类型,然后实现 Error trait。 幸运的是,我们可以用thiserror和anyhow来简化这些步骤。
thiserror 提供了一个派生宏(derive macro)来简化错误类型的定义.
usethiserror::Error;
#[derive(Error,Debug)]
#[non_exhaustive]
pubenumDataStoreError{
#[error("datastoredisconnected")]
Disconnect(#[from]io::Error),
#[error("thedataforkey`{0}`isnotavailable")]
Redaction(String),
#[error("invalidheader(expected{expected:?},found{found:?})")]
InvalidHeader{
expected:String,
found:String,
},
#[error("unknowndatastoreerror")]
Unknown,
}
小结
错误处理的三种方式:使用返回值、异常处理和类型系统。而 Rust 目前看到的方案:主要用类型系统来处理错误,辅以异常来应对不可恢复的错误。
如果你觉得有点收获,欢迎点个关注, 也欢迎分享给你身边的朋友。
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved