传奇世界新开服网站,网站建设未来,做户型图的网站,网站建设工作都包括哪些方面细说Rust错误处理1. 前言这篇文章写得比较长#xff0c;全文读完大约需要15-20min#xff0c;如果对Rust的错误处理不清楚或还有些许模糊的同学#xff0c;请静下心来细细阅读。当读完该篇文章后#xff0c;可以说对Rust的错误处理可以做到掌握自如。笔者花费较长篇幅来描述…细说Rust错误处理1. 前言这篇文章写得比较长全文读完大约需要15-20min如果对Rust的错误处理不清楚或还有些许模糊的同学请静下心来细细阅读。当读完该篇文章后可以说对Rust的错误处理可以做到掌握自如。笔者花费较长篇幅来描述错误处理的来去详细介绍其及一步步梳理内容望大家能耐心读完后对大家有所帮助。当然在写这篇文章之时也借阅了大量互联网资料详见链接见底部参考链接掌握好Rust的错误设计不仅可以提升我们对错误处理的认识对代码结构、层次都有很大的帮助。那废话不多说那我们开启这段阅读之旅吧 2. 背景笔者在写这篇文章时也翻阅一些资料关于Rust的错误处理资料多数是对其一笔带过导致之前接触过其他语言的新同学来说上手处理Rust的错误会有当头棒喝的感觉。找些资料发现unwrap()也可以解决问题然后心中暗自窃喜程序在运行过程中因为忽略检查或程序逻辑判断导致某些情况程序panic。这可能是我们最不愿看到的现象遂又回到起点重新去了解Rust的错误处理。这篇文章通过一步步介绍让大家清晰知道Rust的错误处理的究竟。介绍在Rust中的错误使用及如何处理错误以及在实际工作中关于其使用技巧。3. unwrap的危害!下面我们来看一段代码,执行一下fn main(){letpath/tmp/dat;println!({},read_file(path));}fn read_file(path: str)- String {std::fs::read_to_string(path).unwrap()}程序执行结果thread main panicked at called Result::unwrap() on an Err value: Os { code: 2, kind: NotFound, message: No such file or directory }, src/libcore/result.rs:1188:5stack backtrace:0: backtrace::backtrace::libunwind::traceat /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.40/src/backtrace/libunwind.rs:88...15: rust_sugar::read_fileat src/main.rs:716: rust_sugar::mainat src/main.rs:3...25: rust_sugar::read_filenote: Some details are omitted, run with RUST_BACKTRACEfull for a verbose backtrace.什么因为path路径不对程序竟然崩溃了这个是我们不能接受的unwrap() 这个操作在rust代码中应该看过很多这种代码甚至此时我们正在使用它。它主要用于Option或Result的打开其包装的结果。常常我们在代码中使用简单或快速处理使用了 unwrap() 的操作但是它是一个非常危险的信号!可能因为没有程序检查或校验潜在的bug可能就出现其中使得我们程序往往就panic了。这可能使我们最不愿看到的现象。在实际项目开发中程序中可能充斥着大量代码我们很难避免unwrap()的出现为了解决这种问题我们通过做code review,或使用脚本工具检查其降低其出现的可能性。通常每个项目都有一些约束或许在大型项目开发中 不用unwrap() 方法使用其他方式处理程序unwrap() 的不出现可能会使得程序的健壮性高出很多。这里前提是团队或大型项目如果只是写一个简单例子(demo)就不在本篇文章的讨论范畴。因为一个Demo的问题可能只是快速示范或演示不考虑程序健壮性, unwrap() 的操作可能会更方便代码表达。可能有人会问我们通常跑程序unit test其中的很多mock数据会有 unwrap() 的操作我们只是为了在单元测试中使得程序简单。这种也能不使用吗答案是的完全可以不使用 unwrap() 也可以做到的。4. 对比语言处理错误说到unwrap()我们不得不提到rust的错误处理unwrap() 和Rust的错误处理是密不可分的。4.1 golang的错误处理演示如果了解golang的话应该清楚下面这段代码的意思package mainimport (io/ioutillog)func main() {path : /tmp/dat //文件路径 file, err : readFile(path)if err ! nil {log.Fatal(err) //错误打印 }println(%s, file) //打印文件内容}func readFile(path string) (string, error) {dat, err : ioutil.ReadFile(path) //读取文件内容 if err ! nil { //判断err是否为nil return , err //不为nil,返回err结果 }return string(dat), nil //errnil,返回读取文件内容}我们执行下程序打印如下。执行错误当然因为我们给的文件路径不存在程序报错。2020/02/24 01:24:04 open /tmp/dat: no such file or directory这里golang采用多返回值方式程序报错返回错误问题通过判断 err!nil 来决定程序是否继续执行或终止该逻辑。当然如果接触过golang项目时会发现程序中大量充斥着if err!nil的代码对此网上有对if err!nil进行了很多讨论因为这个不在本篇文章的范畴中在此不对其追溯、讨论。4.2 Rust 错误处理示例对比了golang代码我们对照上面的例子看下在Rust中如何编写这段程序代码如下fn main(){letpath/tmp/dat;//文件路径matchread_file(path){//判断方法结果Ok(file){println!({},file)}//OK 代表读取到文件内容正确打印文件内容Err(e){println!({} {},path,e)}//Err代表结果不存在打印错误结果}}fn read_file(path: str)- Result{//Result作为结果返回值std::fs::read_to_string(path)//读取文件内容}当前因为我们给的文件路径不存在程序报错打印内容如下No such file or directory (os error 2)在Rust代表中Result是一个enum枚举对象,部分源码如下pubenum Result{/// Contains the success valueOk(#[stable(feature rust1, since 1.0.0)]T),/// Contains the error valueErr(#[stable(feature rust1, since 1.0.0)]E),}通常我们使用Result的枚举对象作为程序的返回值通过Result来判断其结果我们使用match匹配的方式来获取Result的内容判断正常(Ok)或错误(Err)。或许我们大致向上看去golang代码和Rust代码没有本质区别都是采用返回值方式给出程序结果。下面我们就对比两种语言说说之间区别golang采用多返回值方式我们在拿到目标结果时(上面是指文件内容file)需要首先对err判断是否为nil,并且我们在return时需要给多返回值分别赋值调用时需要对 if err!nil 做结果判断。Rust中采用Result的枚举对象做结果返回。枚举的好处是多选一。因为Result的枚举类型为Ok和Err使得我们每次在返回Result的结果时要么是Ok,要么是Err。它不需要return结果同时给两个值赋值这样的情况只会存在一种可能性: Ok or Err 。golang的函数调用需要对 if err!nil做结果判断因为这段代码 判断是手动逻辑往往我们可能因为疏忽导致这段逻辑缺失缺少校验。当然我们在编写代码期间可以通过某些工具 lint 扫描出这种潜在bug。Rust的match判断是自动打开当然你也可以选择忽略其中某一个枚举值,我们不在此说明。可能有人发现如果我有多个函数需要多个函数的执行结果这样需要match代码多次代码会不会是一坨一坨显得代码很臃肿难看。是的这个问题提出的的确是有这种问题不过这个在后面我们讲解的时候会通过程序语法糖避免多次match多次结果的问题不过我们在此先不叙说后面将有介绍。5. Rust中的错误处理前面不管是golang还是Rust采用return返回值方式两者都是为了解决程序中错误处理的问题。好了前面说了这么多我们还是回归正题Rust中是如何对错误进行处理的要想细致了解Rust的错误处理我们需要了解std::error::Error该trait的内部方法部分代码如下 参考链接https://doc.rust-lang.org/std/error/trait.Error.htmlpubtraitError: DebugDisplay{fn description(self)- str {description() is deprecated; use Display}#[rustc_deprecated(since 1.33.0, reason replaced by Error::source, which can support \downcasting)]fn cause(self)- Option{self.source()}fn source(self)- Option{None}#[doc(hidden)]fn type_id(self,_: private::Internal)- TypeIdwhereSelf: static{TypeId::of::()}#[unstable(feature backtrace, issue 53487)]fn backtrace(self)- Option{None}}description()在文档介绍中尽管使用它不会导致编译警告但新代码应该实现impl Display 新impl的可以省略不用实现该方法, 要获取字符串形式的错误描述请使用to_string()。cause()在1.33.0被抛弃取而代之使用source()方法新impl的不用实现该方法。source()此错误的低级源如果内部有错误类型Err返回Some(e),如果没有返回None。如果当前Error是低级别的Error,并没有子Error,需要返回None。介于其本身默认有返回值None可以不覆盖该方法。如果当前Error包含子Error,需要返回子ErrorSome(err),需要覆盖该方法。type_id()该方法被隐藏。backtrace()返回发生此错误的堆栈追溯因为标记unstable在Rust的stable版本不被使用。自定义的Error需要impl std::fmt::Debug的trait,当然我们只需要在默认对象上添加注解#[derive(Debug)]即可。总结一下自定义一个error需要实现如下几步手动实现impl std::fmt::Display的trait,并实现 fmt(...)方法。手动实现impl std::fmt::Debug的trait一般直接添加注解即可#[derive(Debug)]手动实现impl std::error::Error的trait,并根据自身error级别是否覆盖std::error::Error中的source()方法。下面我们自己手动实现下Rust的自定义错误:CustomErrorusestd::error::Error;///自定义类型 Error,实现std::fmt::Debug的trait#[derive(Debug)]struct CustomError{err: ChildError,}///实现Display的trait并实现fmt方法implstd::fmt::DisplayforCustomError{fn fmt(self,f: mutstd::fmt::Formatter)- std::fmt::Result{write!(f,CustomError is here!)}}///实现Error的trait,因为有子Error:ChildError,需要覆盖source()方法,返回Some(err)implstd::error::ErrorforCustomError{fn source(self)- Option{Some(self.err)}}///子类型 Error,实现std::fmt::Debug的trait#[derive(Debug)]struct ChildError;///实现Display的trait并实现fmt方法implstd::fmt::DisplayforChildError{fn fmt(self,f: mutstd::fmt::Formatter)- std::fmt::Result{write!(f,ChildError is here!)}}///实现Error的trait,因为没有子Error,不需要覆盖source()方法implstd::error::ErrorforChildError{}///构建一个Result的结果返回自定义的error:CustomErrorfn get_super_error()- Result{Err(CustomError{err: ChildError})}fn main(){matchget_super_error(){Err(e){println!(Error: {},e);println!(Caused by: {},e.source().unwrap());}_println!(No error),}}ChildError为子类型Error,没有覆盖source()方法空实现了std::error::ErrorCustomError有子类型ChildError,覆盖了source(),并返回了子类型Option值Some(self.err)运行执行结果显示如下Error: CustomError is here!Caused by: ChildError is here!至此我们就了解了如何实现Rust中自定义Error了。6. 自定义Error转换:From上面我们说到函数返回Result的结果时需要获取函数的返回值是成功(Ok)还是失败(Err)需要使用match匹配我们看下多函数之间调用是如何解决这类问题的假设我们有个场景 读取一文件 将文件内容转化为UTF8格式 * 将转换后格式内容转为u32的数字。所以我们有了下面三个函数(省略部分代码)...///读取文件内容fn read_file(path: str)- Result{std::fs::read_to_string(path)}/// 转换为utf8内容fn to_utf8(v: [u8])- Result{std::str::from_utf8(v)}/// 转化为u32数字fn to_u32(v: str)- Result{v.parse::()}最终我们得到u32的数字对于该场景如何组织我们代码呢unwrap()直接打开三个方法取出值。这种方式太暴力并且会有bug,造成程序panic,不被采纳。match匹配如何返回OK,继续下一步否则报错终止逻辑那我们试试。参考代码如下:fn main(){letpath./dat;matchread_file(path){Ok(v){matchto_utf8(v.as_bytes()){Ok(u){matchto_u32(u){Ok(t){println!(num:{:?},u);}Err(e){println!({} {},path,e)}}}Err(e){println!({} {},path,e)}}}Err(e){println!({} {},path,e)}}}///读取文件内容fn read_file(path: str)- Result{std::fs::read_to_string(path)}/// 转换为utf8内容fn to_utf8(v: [u8])- Result{std::str::from_utf8(v)}/// 转化为u32数字fn to_u32(v: str)- Result{v.parse::()}天啊虽然是实现了上面场景的需求但是代码犹如叠罗汉程序结构越来越深啊这个是我们没法接受的match匹配导致程序如此不堪一击。那么有没有第三种方法呢当然是有的From转换。前面我们说到如何自定义的Error,如何我们将上面三个error收纳到我们自定义的Error中将它们三个Error变成自定义Error的子Error这样我们对外的Result统一返回自定义的Error。这样程序应该可以改变点什么我们来试试吧。#[derive(Debug)]enum CustomError{ParseIntError(std::num::ParseIntError),Utf8Error(std::str::Utf8Error),IoError(std::io::Error),}implstd::error::ErrorforCustomError{fn source(self)- Option{matchself{CustomError::IoError(refe)Some(e),CustomError::Utf8Error(refe)Some(e),CustomError::ParseIntError(refe)Some(e),}}}implDisplayforCustomError{fn fmt(self,f: mutFormatter)- std::fmt::Result{matchself{CustomError::IoError(refe)e.fmt(f),CustomError::Utf8Error(refe)e.fmt(f),CustomError::ParseIntError(refe)e.fmt(f),}}}implFromforCustomError{fn from(s: std::num::ParseIntError)- Self{CustomError::ParseIntError(s)}}implFromforCustomError{fn from(s: std::io::Error)- Self{CustomError::IoError(s)}}implFromforCustomError{fn from(s: std::str::Utf8Error)- Self{CustomError::Utf8Error(s)}}CustomError为我们实现的自定义ErrorCustomError有三个子类型ErrorCustomError分别实现了三个子类型Error From的trait,将其类型包装为自定义Error的子类型好了有了自定义的CustomError那怎么使用呢? 我们看代码usestd::io::ErrorasIoError;usestd::str::Utf8Error;usestd::num::ParseIntError;usestd::fmt::{Display,Formatter};fn main()- std::result::Result{letpath./dat;letvread_file(path)?;letxto_utf8(v.as_bytes())?;letuto_u32(x)?;println!(num:{:?},u);Ok(())}///读取文件内容fn read_file(path: str)- std::result::Result{std::fs::read_to_string(path)}/// 转换为utf8内容fn to_utf8(v: [u8])- std::result::Result{std::str::from_utf8(v)}/// 转化为u32数字fn to_u32(v: str)- std::result::Result{v.parse::()}#[derive(Debug)]enum CustomError{ParseIntError(std::num::ParseIntError),Utf8Error(std::str::Utf8Error),IoError(std::io::Error),}implstd::error::ErrorforCustomError{fn source(self)- Option{matchself{CustomError::IoError(refe)Some(e),CustomError::Utf8Error(refe)Some(e),CustomError::ParseIntError(refe)Some(e),}}}implDisplayforCustomError{fn fmt(self,f: mutFormatter)- std::fmt::Result{matchself{CustomError::IoError(refe)e.fmt(f),CustomError::Utf8Error(refe)e.fmt(f),CustomError::ParseIntError(refe)e.fmt(f),}}}implFromforCustomError{fn from(s: std::num::ParseIntError)- Self{CustomError::ParseIntError(s)}}implFromforCustomError{fn from(s: std::io::Error)- Self{CustomError::IoError(s)}}implFromforCustomError{fn from(s: std::str::Utf8Error)- Self{CustomError::Utf8Error(s)}}其实我们主要关心的是这段代码fn main()- Result{letpath./dat;letvread_file(path)?;letxto_utf8(v.as_bytes())?;letuto_u32(x)?;println!(num:{:?},u);Ok(())}我们使用了?来替代原来的match匹配的方式。?使用问号作用在函数的结束意思是程序接受了一个Result自定义的错误类型。当前如果函数结果错误程序自动抛出Err自身错误类型并包含相关自己类型错误信息因为我们做了From转换的操作该函数的自身类型错误会通过实现的From操作自动转化为CustomError的自定义类型错误。当前如果函数结果正确继续之后逻辑直到程序结束。这样我们通过From和?解决了之前match匹配代码层级深的问题因为这种转换是无感知的使得我们在处理好错误类型后只需要关心我们的目标值即可这样不需要显示对Err(e)的数据单独处理使得我们在函数后添加?后程序一切都是自动了。还记得我们之前讨论在对比golang的错误处理时的:if err!nil的逻辑了吗这种因为用了?语法糖使得该段判断将不再存在。另外我们还注意到Result的结果可以作用在main函数上是的Result的结果不仅能作用在main函数上Result还可以作用在单元测试上这就是我们文中刚开始提到的因为有了Result的作用使得我们在程序中几乎可以完全摒弃unwrap()的代码块使得程序更轻大大减少潜在问题程序组织结构更加清晰。下面这是作用在单元测试上的Result的代码...#[cfg(test)]mod tests{usesuper::*;#[test]fn test_get_num()- std::result::Result{letpath./dat;letvread_file(path)?;letxto_utf8(v.as_bytes())?;letuto_u32(x)?;assert_eq!(u,8);Ok(())}}7. 重命名Result我们在实际项目中会大量使用如上的Result结果并且Result的Err类型是我们自定义错误,导致我们写程序时会显得非常啰嗦、冗余///读取文件内容fn read_file(path: str)- std::result::Result{letvalstd::fs::read_to_string(path)?;Ok(val)}/// 转换为utf8内容fn to_utf8(v: [u8])- std::result::Result{letxstd::str::from_utf8(v)?;Ok(x)}/// 转化为u32数字fn to_u32(v: str)- std::result::Result{letiv.parse::()?;Ok(i)}我们的程序中会大量充斥着这种模板代码Rust本身支持对类型自定义使得我们只需要重命名Result即可:pubtype IResultstd::result::Result;///自定义Result类型IResult这样凡是使用的是自定义类型错误的Result都可以使用IResult来替换std::result::Result的类型使得简化程序隐藏Error类型及细节关注目标主体代码如下///读取文件内容fn read_file(path: str)- IResult{letvalstd::fs::read_to_string(path)?;Ok(val)}/// 转换为utf8内容fn to_utf8(v: [u8])- IResult{letxstd::str::from_utf8(v)?;Ok(x)}/// 转化为u32数字fn to_u32(v: str)- IResult{letiv.parse::()?;Ok(i)}将std::result::Result 替换为IResult类型当然会有人提问如果是多参数类型怎么处理呢同样我们只需将OK类型变成 tuple (I,O)类型的多参数数据即可大概这样pubtype IResultstd::result::Result;使用也及其简单只需要返回I,O的具体类型,举个示例fn foo()- IResult{Ok((String::from(bar),32))}使用重命名类型的Result使得我们错误类型统一方便处理。在实际项目中可以大量看到这种例子的存在。8. Option转换我们知道在Rust中需要使用到unwrap()的方法的对象有Result,Option对象。我们看下Option的大致结构pubenum Option{/// No value#[stable(feature rust1, since 1.0.0)]None,/// Some value T#[stable(feature rust1, since 1.0.0)]Some(#[stable(feature rust1, since 1.0.0)]T),}Option本身是一个enum对象如果该函数(方法)调用结果值没有值返回None,反之有值返回Some(T)如果我们想获取Some(T)中的T,最直接的方式是unwrap()。我们前面说过使用unwrap()的方式太过于暴力如果出错程序直接panic这是我们最不愿意看到的结果。Ok,那么我们试想下, 利用Option能使用?语法糖吗如果能用?转换的话是不是代码结构就更简单了呢我们尝试下,代码如下#[derive(Debug)]enum Error{OptionError(String),}implstd::error::ErrorforError{}implstd::fmt::DisplayforError{fn fmt(self,f: mutstd::fmt::Formatter)- std::fmt::Result{matchself{Error::OptionError(refe)e.fmt(f),}}}pubtype Resultstd::result::Result;fn main()- Result{letbarfoo(60)?;assert_eq!(bar,bar);Ok(())}fn foo(index: i32)- Option{ifindex60{returnSome(bar.to_string());}None}执行结果报错error[E0277]: ? couldnt convert the error to Error-- src/main.rs:22:22|22 | let bar foo(60)?;| ^ the trait std::convert::From:option::noneerror is not implemented for Error| note: the question mark operation (?) implicitly performs a conversion on the error value using the From trait note: required by std::convert::From::fromerror: aborting due to previous errorFor more information about this error, try rustc --explain E0277.error: could not compile hyper-define.提示告诉我们没有转换std::convert::From:option::noneerror但是NoneError本身是unstable这样我们没法通过From转换为自定义Error。本身在Rust的设计中关于Option和Result就是一对孪生兄弟一样的存在Option的存在可以忽略异常的细节直接关注目标主体。当然Option也可以通过内置的组合器ok_or()方法将其变成Result。我们大致看下实现细节implOption{pubfn ok_or(self,err: E)- Result{matchself{Some(v)Ok(v),NoneErr(err),}}}这里通过ok_or()方法通过接收一个自定义Error类型将一个Option-Result。好的变成Result的类型我们就是我们熟悉的领域了这样处理起来就很灵活。关于Option的其他处理方式不在此展开解决详细的可看下面链接9. 避免unwrap()有人肯定会有疑问如果需要判断的逻辑又不用?这种操作怎么取出Option或Result的数据呢当然点子总比办法多我们来看下Option如何做的fn main(){ifletSome(v)opt_val(60){println!({},v);}}fn opt_val(num: i32)- Option{ifnum60{returnSome(foo bar.to_string());}None}是的我们使用if let Some(v)的方式取出值当前else的逻辑就可能需要自己处理了。当然Option可以这样做Result也一定可以:fn main(){ifletOk(v)read_file(./dat){println!({},v);}}fn read_file(path: str)- Result{std::fs::read_to_string(path)}只不过在处理Result的判断时使用的是if let Ok(v)这个和Option的if let Some(v)有所不同。到这里unwrap()的代码片在项目中应该可以规避了。补充下这里强调了几次规避就如前所言团队风格统一方便管理代码消除潜在危机。10. 自定义Error同级转换我们在项目中一个函数(方法)内部会有多次Result的结果判断?,假设我们自定义的全局Error名称为GlobalError。这时候如果全局有一个Error可能就会出现如下错误std::convert::From:globalerrorisnotimplementedforerror::GlobalError意思是我们自定义的GlobalError没有通过From转换我们自己自定义的GlobalError那这样就等于自己转换自己。注意第一这是我们不期望这样做的。第二遇到这种自己转换自己的T类型很多我们不可能把出现的T类型通通实现一遍。 这时候我们考虑自定义另一个Error了假设我们视为InnnerError,我们全局的Error取名为GlobalError我们在遇到上面错误时返回Result,这样我们遇到Result时只需要通过From转换即可代码示例如下implFromforGlobalError{fn from(s: InnerError)- Self{Error::new(ErrorKind::InnerError(e))}}上面说的这种情况可能会在项目中出现多个自定义Error,出现这种情况时存在多个不同Error的std::result::Result的返回。这里的Err就可以根据我们业务现状分别反回不同类型了。最终只要实现了From的trait可转化为最终期望结果。11. Error常见开源库好了介绍到这里我们应该有了非常清晰的认知关于如何处理Rust的错误处理问题了。但是想想上面的这些逻辑多数是模板代码我们在实际中大可不必这样。说到这里开源社区也有了很多对错误处理库的支持下面列举了一些12. 参考链接13 错误处理实战这个例子介绍了如何在https://github.com/Geal/nom中处理错误这里就不展开介绍了有兴趣的可自行阅读代码。14. 总结好了经过上面的长篇大论不知道大家是否明白如何自定义处理Error呢了。大家现在带着之前的已有的问题或困惑赶紧实战下Rust的错误处理吧大家有疑问或者问题都可以留言我希望这篇文章对你有帮助。