Rust学习笔记Day21 为什么Rust的错误处理与众不同?

Rust学习笔记Day21 为什么Rust的错误处理与众不同?

首页游戏大全Rust Red更新时间:2024-06-07

语言优秀的错误处理能力,会大大减少错误对整体流程的破坏,减少我们码农的心智负担。

我们一般处理错误的流程:

  1. 当错误发生时,用合适的错误类型捕获错误。
  2. 捕获到错误后,可以立刻处理,也可以延迟在处理。
  3. 根据不同的错误,返回给用户不同的错误消息。
错误处理的主流方法

主要有三种方法:

一、使用返回值(错误码)

有很多例子 比如:

在 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 目前看到的方案:主要用类型系统来处理错误,辅以异常来应对不可恢复的错误。

  1. 相比 C/Golang 直接用返回值的错误处理方式,Rust 在类型上更完备,构建了逻辑更为严谨的 Option 类型和 Result 类型,既避免了错误被不慎忽略,也避免了用啰嗦的表达方式传递错误;
  2. 相对于 C / Java 使用异常的方式,Rust 区分了可恢复错误和不可恢复错误,分别使用 Option / Result,以及 panic! / catch_unwind 来应对,更安全高效,避免了异常安全带来的诸多问题;
  3. 对比它的老师 Haskell,Rust 的错误处理更加实用简洁,这得益于它强大的元编程功能,使用 ?操作符来简化错误的传递。

如果你觉得有点收获,欢迎点个关注, 也欢迎分享给你身边的朋友。



查看全文
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved