错误处理(Error Handling)

我早就知道无论我活多久,这种事情迟早总会发生的 —George Bernard Shaw on dying

原文

I knew if I stayed around long enough, something like this would happen

Rust中的错误处理与其自身的简短章节不同.这里没有任何困难的想法,只是对你来说可能不熟悉.本章介绍Rust中的两种不同类型的错误处理:panic和Result.

使用Result处理普通错误.这些通常是由程序外的事情引起的,例如错误的输入,网络中断或权限问题.发生这种情况并不取决于我们;即使是无错误的程序也会不时遇到它们.本章的大部分内容都是针对这种错误的.不过,我们会介绍panic,因为它更简单.

panic是针对另一种错误,那种 不应该发生(should never happen) 的错误.

Panic

一个程序在遇到一些非常混乱的事情时会恐慌(panics),程序本身一定存在错误.就像是:

  • 数组访问越界

  • 整数除以零

  • 在碰巧为NoneOption上调用.unwrap()

  • 断言失败

(还有宏panic!(),对于你自己的代码发现它出错的情况,你因此需要直接触发panic.panic!()接受可选的println!()式参数,用于构建错误信息.)

这些条件的共同之处在于它们都是—不够细致—程序员的错误.一个好的经验法则是:”Don’t panic”.

但我们都会犯错.当这些不该发生的错误确实发生了—然后呢?值得注意的是,Rust给了你一个选择.当发生恐慌时,Rust可以展开堆栈,也可以中止进程.展开是默认的.

展开(Unwinding)

当海盗瓜分从突袭中所得战利品时,船长获得了一半的战利品.普通船员获得另一半的平等份额.(海盗讨厌分数,所以如果任何一个分裂都没有出现,结果会向下舍入,其余部分将归船上的鹦鹉.)

  1. fn pirate_share(total: u64, crew_size: usize) -> u64 {
  2. let half = total / 2;
  3. half / crew_size as u64
  4. }

这种情况可能会持续几个世纪,直到有一天发现船长是突袭的唯一幸存者.如果我们将一个为零的crew_size传递给这个函数,它将除以零.在C++中,这将是未定义行为.在Rust中,它会引发恐慌(panic),通常如下所示进行:

  • 将错误消息打印到终端:
  1. thread 'main' panicked at 'attempt to divide by zero', pirates.rs:3780
  2. note: Run with `RUST_BACKTRACE=1` for a backtrace.

如果设置RUST_BACKTRACE环境变量,正如消息所示,Rust也会在此时转储堆栈.

  • 堆栈已解开.这很像C++异常处理.

当前函数正在使用的任何临时值,局部变量或参数将按照与创建它们相反的顺序顺序被删除.删除值只是意味着在它之后进行清理:程序使用的任何StringVec都被释放,任何打开的File都被关闭,等等.也调用用户定义的drop方法;请参阅第282页的”Drop”.在pirate_share()的特定情况下,没有什么可以清理的.

清除当前函数调用后,我们继续它的调用者,以相同的方式删除它的变量和参数.然后 这个(that) 函数的调用者,依此类推.

  • 最后,线程退出.如果恐慌线程是主线程,则整个进程退出(使用非零退出代码).

也许 恐慌(panic) 是这个有序过程的误导性名称.恐慌不是崩溃.这不是未定义行为.它更像是Java中的RuntimeException或C++中的std::logic_error.行为是定义明确的;它不应该发生.

恐慌是安全的.它不违反Rust的任何安全规则;即使你在标准库方法的中间设法恐慌,它也永远不会在内存中留下悬空指针或半初始值.我们的想法是,在发生任何不良事件 之前(before) ,Rust会捕获无效的数组访问,或者不管它是什么.继续进行是不安全的,所以Rust解开了堆栈.但其余的过程可以继续运行.

每个线程都有恐慌.一个线程可能会恐慌,而其他线程正在进行正常业务.在第19章中,我们将展示父线程如何在子线程发生恐慌时找出它并优雅地处理错误.

还有一种方法可以捕获堆栈展开,允许线程存活并继续运行.标准库函数std::panic::catch_unwind()就是这样做的.我们不会介绍如何使用它,但这是Rust的测试线程在测试中断言失败时恢复的机制.(在编写可以从C或C++调用的Rust代码时,也有必要这样做,因为跨非Rust代码中展开是未定义行为;请参阅第21章.)

理想情况下,我们都将拥有永不恐慌的无错误代码.但没有人是完美的.你可以使用线程和catch_unwind()来处理恐慌,使你的程序更加健壮.一个重要的警告是,这些工具只能捕获展开堆栈的恐慌.并非每次恐慌都是这样进行的.

中止(Aborting)

堆栈展开是默认的恐慌行为,但有两种情况下Rust不会尝试展开堆栈.

如果在第一次恐慌之后一个.drop()方法触发第二次恐慌而Rust仍在尝试清理,这被认为是致命的.Rust停止展开并中止整个过程.

此外,Rust的恐慌行为是可定制的.如果使用-C panic = abort进行编译,程序中的 第一个(first) 恐慌会立即中止该过程.(使用此选项,Rust不需要知道如何展开堆栈,因此这可以减少编译完的代码的大小).

这就结束了我们对Rust的恐慌的讨论.没有太多可说的,因为普通的Rust代码没有义务处理恐慌.即使你使用线程或catch_unwind(),所有恐慌处理代码也可能集中在少数几个地方.期望程序中的每个函数都能预测并处理自己代码中的错误是不合理的.其他因素造成的错误是另一个严重的问题.

Result(Result)

Rust没有异常.相反,可能失败的函数有一个返回类型,如下所示:

  1. fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>

Result类型表示可能的失败.当我们调用get_weather()函数时,它将返回 成功结果(success result) Ok(weather),weather是一个新的WeatherReport值,或返回 错误结果(error result) ,Err(error_value),其中error_value是一个io::Error解释出了什么错.

Rust要求我们在调用此函数时编写某种错误处理.我们无法在没有对Result某些(something) 事情的情况下获得WeatherReport,如果未使用Result值,你将收到编译器警告.

在第10章中,我们将看到标准库如何定义Result以及如何定义自己的类似类型.现在,我们将采用”烹饪手册(cookbook)”方法,并专注于如何使用Result来获得你想要的错误处理行为.

捕获错误(Catching Errors)

处理Result的最彻底的方法是我们在第2章中展示的方式:使用match表达式.

  1. match get_weather(hometown) {
  2. Ok(report) => {display_weather(hometown, &report);
  3. }
  4. Err(err) => {
  5. println!("error querying the weather: {}", err);
  6. schedule_weather_retry();
  7. }
  8. }

这是Rust相当于其他语言的try/catch.这是你想要正面处理错误时使用的,而不是将它们传递给你的调用者.

match有点冗长,因此Result<T, E>提供了各种在特殊情况下有用的方法.这些方法中的每一个在其实现中都具有match表达式.(有关结果方法的完整列表,请参阅在线文档.此处列出的方法是我们最常用的方法.)

  • result.is_ok()result.is_err()返回一个bool,告诉结果是成功结果还是错误结果.

  • result.ok()返回成功值(如果有)作为Option<T>.如果result是成功结果,则返回Some(success_value);否则,它返回None,丢弃错误值.

  • result.err()返回错误值(如果有),作为Option<E>.

  • result.unwrap_or(fallback)返回成功值,如果result是成功结果.否则,它返回fallback,丢弃错误值.

  1. // A fairly safe prediction for Southern California.
  2. const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);
  3. // Get a real weather report, if possible.
  4. // If not, fall back on the usual.
  5. let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
  6. display_weather(los_angeles, &report);

这是.ok()的一个很好的替代方法,因为返回类型是T,而不是Option<T>.当然,只有在有适当的后备值时它才有效.

  • result.unwrap_or_else(fallback_fn)是相同的,但不是直接传递一个后备值,而是传递一个函数或闭包.这适用于如果你不打算使用它来计算后备值会浪费的情况.仅当我们有错误结果时才会调用fallback_fn.
  1. let report =
  2. get_weather(hometown)
  3. .unwrap_or_else(|_err| vague_prediction(hometown));

(第14章详细介绍闭包)

  • result.unwrap()也返回成功值,如果result是成功结果.但是,如果result是错误结果,则此方法会发生恐慌.这种方法有其用途;我们稍后会详细讨论它.

  • result.expect(message).unwrap()相同,但允许你提供在出现恐慌情况时打印的消息.

最后,有两种方法可以借用对Result中值的引用:

  • result.as_ref()Result<T, E>转换为Result<&T, &E>,借用对现有结果中成功或错误值的引用.

  • result.as_mut()是相同的,但借用了一个可变引用.返回类型是Result<&mut T, &mut E>.

这两个方法最有用的一个原因是此处列出的所有其他方法(.is_ok().is_err()除外)都会 消耗(consume) 它们操作的result.也就是说,他们通过值来接受self参数.有时在不破坏结果的情况下访问结果中的数据非常方便,这就是.as_ref().as_mut()为我们做的事情.

例如,假设你想调用result.ok(),但你需要将结果保持原样.你可以编写result.as_ref().ok(),它只是借用结果,返回Option<&T>而不是Option<T>.

Result类型别名(Result Type Aliases)

有时你会看到Rust文档似乎省略了Result的错误类型:

  1. fn remove_file(path: &Path) -> Result<()>

这意味着正在使用Result类型别名.

类型别名是类型名称的一种简写.模块通常定义Result类型别名,以避免重复模块中几乎每个函数使用一致的错误类型.例如,标准库的std::io模块包含以下代码行:

  1. pub type Result<T> = result::Result<T, Error>;

这定义了一个公开类型std::io::Result<T>.它是Result<T, E>的别名,但硬编码std::io::Error作为错误类型.实际上,这意味着如果你编写use std::io;然后Rust会将io::Result<String>理解为Result<String, io::Error>的简写.

当在线文档中出现Result<()>之类的内容时,你可以单击标识符Result来查看正在使用的类型别名并了解错误类型.在实践中,它通常从上下文中显而易见.

打印错误(Printing Errors)

有时,处理错误的唯一方法是将其转储到终端并继续.我们已经展示了一种方法:

  1. println!("error querying the weather: {}", err);

标准库定义了几个具有无聊名称的错误类型:std::io::Error,std::fmt::Error,std::str::Utf8Error,等等.所有这些都实现了一个通用接口,即std::error::Errortrait,这意味着它们共享以下特性:

  • 它们都可以使用println!()打印.使用{}格式说明符打印错误通常仅显示简短的错误消息.或者,你可以使用{:?}格式说明符进行打印,以获取错误的Debug视图.这不是用户友好的,但包括额外的技术信息.
  1. // result of `println!("error: {}", err);`
  2. error: failed to lookup address information: No address associated with
  3. hostname
  4. // result of `println!("error: {:?}", err);`
  5. error: Error { repr: Custom(Custom { kind: Other, error: StringError(
  6. "failed to lookup address information: No address associated with
  7. hostname") }) }
  • err.description()&str形式返回错误消息.

  • err.cause()返回一个Option<&Error>:)触发err的底层错误(如果有).

  • 例如,网络错误可能导致银行交易失败,这可能反过来导致你的船被重新收回.如果err.description()"boat is repossessed",则err.cause()可能会返回有关失败事务的错误;它的.description()可能是"failed to transfer $300 to United Yacht Supply",其.cause()可能是一个io::Error,其中包含导致所有大惊小怪的特定网络中断的详细信息. 第三个错误是根本原因,因此其.cause()方法将返回None.

由于标准库仅包含相当低级的功能,因此标准库错误通常为None.

打印错误值也不会打印出原因.如果您想确保打印所有可用信息,请使用此函数:

  1. use std::error::Error;
  2. use std::io::{Write, stderr};
  3. /// Dump an error message to `stderr`.
  4. ///
  5. /// If another error happens while building the error message or
  6. /// writing to `stderr`, it is ignored.
  7. fn print_error(mut err: &Error) {
  8. let _ = writeln!(stderr(), "error: {}", err);
  9. while let Some(cause) = err.cause() {
  10. let _ = writeln!(stderr(), "caused by: {}", cause);
  11. err = cause;
  12. }
  13. }

标准库的错误类型不包含堆栈跟踪.但error-chaincrate可以轻松定义自己的自定义错误类型,该类型支持在创建堆栈跟踪时获取堆栈跟踪.它使用backtracecrate来捕获堆栈.

传播错误(Propagating Errors)

在我们尝试一些可能失败的大多数地方,我们不希望立即捕获并处理错误.在每个可能出错的地方使用10行match语句的代码太多了.

相反,如果发生错误,我们通常希望让我们的调用者处理它.我们希望错误 传播(propagate) 到调用堆栈.

Rust有一个?运算符执行此操作.你可以添加一个?到任何产生Result的表达式,例如函数调用的结果:

  1. let weather = get_weather(hometown)?

?的行为取决于此函数是返回成功结果还是错误结果:

  • 成功时,它会打开Result以获得成功值.这里的weather的类型不是Result<WeatherReport,io::Error>,而只是WeatherReport.

  • 出错时,它立即从封闭函数返回,将错误结果传递给调用链.为了确保这一点,?只能在具有Result返回类型的函数中使用.

?操作符没有什么神奇之处.你可以使用match表达式表达相同的内容,尽管它更加冗长:

  1. let weather = match get_weather(hometown) {
  2. Ok(success_value) => success_value,
  3. Err(err) => return Err(err)
  4. };

这个和?唯一的区别是运算符是涉及类型和转换的一些细节.我们将在下一节中介绍这些细节.

在较旧的代码中,你可能会看到try!()宏,这是传播错误的常用方法,直到?运算符在Rust 1.13中引入.

  1. let weather = try!(get_weather(hometown));

宏展开为match表达式,和上面的一样.

很容易忘记程序中错误的可能性是多么普遍,特别是在与操作系统接口的代码中.?运算符有时会显示在函数的几乎每一行上:

  1. use std::fs;
  2. use std::io;
  3. use std::path::Path;
  4. fn move_all(src: &Path, dst: &Path) -> io::Result<()> {
  5. for entry_result in src.read_dir()? { // opening dir could fail
  6. let entry = entry_result?; // reading dir could fail
  7. let dst_file = dst.join(entry.file_name());
  8. fs::rename(entry.path(), dst_file)?; // renaming could fail
  9. }
  10. Ok(()) // phew!
  11. }

使用多种错误类型(Working with Multiple Error Types)

通常,不止一件事可能出错.假设我们只是从文本文件中读取数字.

  1. use std::io::{self, BufRead};
  2. /// Read integers from a text file.
  3. /// The file should have one number on each line.
  4. fn read_numbers(file: &mut BufRead) -> Result<Vec<i64>, io::Error> {
  5. let mut numbers = vec![];
  6. for line_result in file.lines() {
  7. let line = line_result?; // reading lines can fail
  8. numbers.push(line.parse()?); // parsing integers can fail
  9. }
  10. Ok(numbers)
  11. }

Rust给我们一个编译器错误:

  1. numbers.push(line.parse()?); // parsing integers can fail
  2. ^^^^^^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>`
  3. is not implemented for `std::io::Error`

当我们达到涵盖trait的第11章时,此错误消息中的术语将更有意义.现在,请注意Rust抱怨它无法将std::num::ParseIntError值转换为std::io::Error类型.

这里的问题是从文件中读取一行并解析整数会产生两种不同的潜在错误类型.line_result的类型是Result<String, std::io::Error>.line.parse()的类型是Result<i64, std::num::ParseIntError>.我们的read_numbers()函数的返回类型只适用于io::Error.Rust试图通过将它转换为io::Error来处理ParseIntError,但是没有这样的转换,所以我们得到了一个类型错误.

有几种方法可以解决这个问题.例如,我们在第2章中用来创建Mandelbrot集的图像文件的图像包定义了它自己的错误类型ImageError,并实现了从io::Error和其他几种错误类型到ImageError的转换.如果你想走这条路,试试上面提到的错误error-chaincrate,它旨在帮助你用几行代码定义好的错误类型.

一种更简单的方法是使用Rust内置的内容.所有标准库错误类型都可以转换为Box<std::error::Error>类型,它代表”任何错误(any error).”因此,处理多种错误类型的简单方法是定义这些类型别名:

  1. type GenError = Box<std::error::Error>;
  2. type GenResult<T> = Result<T, GenError>;

然后,将read_numbers()的返回类型更改为GenResult<Vec<i64 >>.通过此更改,函数可以编译.?操作符根据需要自动将任何类型的错误转换为`GenError.

顺便说一句,?操作符使用你自己可以使用的标准方法进行自动转换.要将任何错误转换为GenError类型,请调用GenError::from():

  1. let io_error = io::Error::new( // make our own io::Error
  2. io::ErrorKind::Other, "timed out");
  3. return Err(GenError::from(io_error
  4. )); // manually convert to GenError

我们将在第13章中完整地介绍Fromtrait及其from()方法.

GenError方法的缺点是返回类型不再准确地传达调用者可以预期的错误类型.调用者必须做好一切准备.

如果你正在调用一个返回GenResult的函数,并且你希望处理一种特定类型的错误,但让所有其他错误传播出去,请使用泛型方法error.downcast_ref::<ErrorType>().它借用了对错误的引用, 如果(if) 碰巧是你正在寻找的特定类型的错误:

  1. loop {
  2. match compile_project() {
  3. Ok(()) => return Ok(()),
  4. Err(err) => {
  5. if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
  6. insert_semicolon_in_source_code(mse.file(), mse.line())?;
  7. continue; // try again!
  8. }
  9. return Err(err);
  10. }
  11. }
  12. }

许多语言都有内置的语法来实现这一点,但事实证明很少需要这样做.Rust有一个替代方法.

处理”不会发生”的错误(Dealing with Errors That Can’t Happen”)

有时我们只 知道(know) 错误不会发生.例如,假设我们正在编写代码来解析配置文件,并且我们发现文件中的下一个东西是数字字符串:

  1. if next_char.is_digit(10) {
  2. let start = current_index;
  3. current_index = skip_digits(&line, current_index);
  4. let digits = &line[start..current_index];
  5. ...

我们想将这个数字字符串转换为实际数字.有一种标准方法可以做到这一点:

  1. let num = digits.parse::<u64>();

现在问题是:str.parse::<u64>()方法不返回u64.它返回一个Result.它可能会失败,因为某些字符串不是数字.

  1. "bleen".parse::<u64>() // ParseIntError: invalid digit

但我们碰巧知道在这种情况下,digits完全由数字组成.我们应该做什么?

如果我们编写的代码已经返回GenResult,我们可以使用?并忘记它.否则,我们面临着必须为不会发生的错误编写错误处理代码的恼人前景.那么最好的选择是使用.unwrap(),我们前面提到的Result方法.

  1. let num = digits.parse::<u64>().unwrap();

这就像?但如果我们对这个错误的判断是错误的,如果它 可能(can) 发生,那么在那种情况下我们就会恐慌.

事实上,我们对这个特殊情况的看法是错误的.如果输入包含足够长的数字串,那么这个数字将大到无法放入u64中.

  1. "99999999999999999999".parse::<u64>() // overflow error

因此在这种特殊情况下使用.unwrap()将是一个错误.虚假输入不应引起恐慌.

也就是说,在Result值确实不能成为错误的情况下会出现这种情况.例如,在第18章中,你将看到Writetrait为文本和二进制输出定义了一组通用方法(.write()和其他方法).所有这些方法都返回io::Result,但是如果你正好写入Vec<u8>,它们就不会失败.在这种情况下,可以使用.unwrap().expect(message)来省略Result.

当错误表明条件如此严重或奇怪以至于恐慌正是您想要处理它时,这些方法也很有用.

  1. fn print_file_age(filename: &Path,last_modified: SystemTime) {
  2. let age = last_modified.elapsed().expect("system clock drift");
  3. ...
  4. }

这里,.elapsed()方法只有在系统时间早于创建文件时才会失败.如果文件是最近创建的,并且系统时钟在我们的程序运行时向后调整,则会发生这种情况.根据这段代码的使用方式,在这种情况下,它是一种合理的判断调用,而不是处理错误或将其传播给调用者.

忽略错误(Ignoring Errors)

偶尔我们只想完全忽略错误.例如,在我们的print_error()函数中,我们必须处理打印错误触发另一个错误的不太可能的情况.例如,如果将stderr传送到另一个进程,该进程被终止,可能发生这种情况.由于关于这种错误我们无能为力,我们只想忽略它;但Rust编译器会警告未使用的Result值:

  1. writeln!(stderr(), "error: {}", err); // warning: unused result

习语let _ = ...用来沉默这个警告:

  1. let _ = writeln!(stderr(), "error: {}", err); // ok, ignore result

处理main()中的错误(Handling Errors in main())

在产生Result的大多数地方,让错误冒泡到调用者是正确的行为.这就是为什么?是Rust的单个字符.正如我们所看到的,在某些程序中,它连续使用多行代码.

但是如果你传播一个足够长的错误,最终它会到达main()而这就是这种方法必须停止的地方.main()不能使用?因为它的返回类型不是Result.

  1. fn main() {
  2. calculate_tides()?; // error: can't pass the buck any further
  3. }

处理main()中错误的最简单方法是使用.expect().

  1. fn main() {
  2. calculate_tides().expect("error"); // the buck stops here
  3. }

如果calculate_tides()返回错误结果,则.expect()方法会发生恐慌.主线程中的恐慌会打印出错误消息,然后以非零退出代码退出,这大致是所需的行为.我们一直在小程序中这样用.这是一个开始.

但错误信息有点令人生畏:

  1. $ tidecalc --planet mercury
  2. thread 'main' panicked at 'error: "moon not found"', /buildslave/rust-buildbot/s
  3. lave/nightly-dist-rustc-linux/build/src/libcore/result.rs:837
  4. note: Run with `RUST_BACKTRACE=1` for a backtrace.

错误消息丢失在噪音中.另外,在这种特殊情况下,RUST_BACKTRACE = 1是不好的建议.自己打印错误消息是值得的:

  1. fn main() {
  2. if let Err(err) = calculate_tides() {
  3. print_error(&err);
  4. std::process::exit(1);
  5. }
  6. }

仅当对calculate_tides()的调用返回错误结果时,此代码才使用if let表达式来打印错误消息.有关if let表达式的详细信息,请参阅第10章. print_error函数在”打印错误(Printing Errors)”(第150页)中列出.

现在输出漂亮整洁:

  1. $ tidecalc --planet mercury
  2. error: moon not found

声明自定义错误类型(Declaring a Custom Error Type)

假设你正在编写一个新的JSON解析器,并且你希望它有自己的错误类型.(我们还没有介绍用户定义的类型;后面几章会讲到.但错误类型很方便,所以我们将在这里包含一些预览.)

大概你要编写的最少代码是:

  1. // json/src/error.rs
  2. #[derive(Debug, Clone)]
  3. pub struct JsonError {
  4. pub message: String,
  5. pub line: usize,
  6. pub column: usize,
  7. }

这个结构将被称为json::error::JsonError,当你想引发这种类型的错误时,你可以写:

  1. return Err(JsonError {
  2. message: "expected ']' at end of array".to_string(),
  3. line: current_line,
  4. column: current_column
  5. });

这样可以正常工作.但是,如果你希望你的错误类型像标准错误类型一样工作,就像你的库的用户所期望的那样,那么你还需要做更多的工作:

  1. use std;
  2. use std::fmt;
  3. // Errors should be printable.
  4. impl fmt::Display for JsonError {
  5. fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
  6. write!(f, "{} ({}:{})", self.message, self.line, self.column)
  7. }
  8. }
  9. // Errors should implement the std::error::Error trait.
  10. impl std::error::Error for JsonError {
  11. fn description(&self) -> &str {
  12. &self.message
  13. }
  14. }

同样,impl关键字,self和所有其他内容的含义将在接下来的几章中解释.

为什么Result?(Why Result)

现在我们已经足够了解Rust选择Result而不是异常获得了什么.以下是设计的要点:

  • Rust要求程序员做出某种决定,并在可能发生错误的每一点将其记录在代码中.这很好,因为不然的话,由于疏忽很容易导致错误处理错误.

  • 最常见的决定是允许错误传播,并用单个字符(‘?’)编写.因此,错误管道不会像在C和Go中那样混乱代码.然而它仍然可见:你可以查看一大块代码,并一目了然地查看传播错误的所有位置.

  • 由于错误的可能性是每个函数的返回类型的一部分,因此很清楚哪些函数可能会失败,哪些函数不能.如果将函数更改为可能出错,则会更改其返回类型,因此编译器将使你更新该函数的下游用户.

  • Rust会检查Result值是否被使用,因此你不会无意中让错误无声地传递(C中的常见错误).

  • 由于Result是一种与其他类型一样的数据类型,因此很容易将成功和错误结果存储在同一个集合中.这使得建模部分成功变得容易.例如,如果你正在编写一个从文本文件中加载数百万条记录的程序,并且你需要一种方法来应对处理可能的结果,即大多数会成功,但有些会失败,你可以使用Result的向量在内存中表示这种情况.

这样做的代价是,你会发现自己在Rust中考虑和设计错误处理比在其他语言中更多.和许多其他领域一样,Rust在错误处理方面的表现比你习惯的要紧一些.对于系统编程来说,这是值得的.