带有结果的可恢复错误
大多数错误并不严重到需要程序完全停止。有时,当一个函数失败时,其原因可以很容易地解释和响应。例如,如果您尝试打开一个文件,但由于该文件不存在而导致该操作失败,那么您可能希望创建该文件,而不是终止该进程。
回到第二章Result
的枚举定义里,有两个成员,Ok
和Err
,如下:
1 | enum Result<T, E> { |
T
和E
是泛型参数:我们将在第十章详细讨论。目前你需要知道当成功时返回枚举成员Ok
,当失败时返回枚举成员Err
。由于Result
具有这些泛型类型参数,因此我们可以在许多不同的情况下使用Result
类型及其上定义的函数,其中我们希望返回的成功值和错误值可能不同。
我们来调用一个返回值类型为Result
的函数,因为调用可能会失败。如Listing 9-3我们尝试打开一个文件。
Filename: src/main.rs
1 | use std::fs::File; |
Listing 9-3 Openging a file
File::open
的返回值是一个Result<T, E>
。泛型参数T
已经被File::open
实现,并填入了成功的值,std::fs::File
是一个文件句柄(file handle)。类型E
的错误值是std::io::Error
。File::open
返回类型意思是如果成功就会返回一个文件句柄,并且可以进行读写。这个函数也有可能会调用失败:例如,文件不存在,或者没有权限访问。File::open
函数需要有个方式告诉我们是成功或者失败,同时返回给我们文件句柄或者错误信息。这个信息正是Result枚举所传达的。
因此,当File::open
调用成功,变量greeting_file_result
的值将会是成员Ok
并包含一个文件句柄。如果失败,greeting_file_result
就是一个Err
的实例并包含更多错误信息来展示到底发生了什么错误。
我们需要在Listing9-3中根据File::open
的返回值来添加额外的代码。如Listing9-4中所示,这是一个基本的Result
的处理工具,就是使用match
表达式(我们已经在第六章中讲过了)。
Filename:src/main.rs
1 | use std::fs::File; |
Listing 9-4: Using a match expression to handle the Result
variants that might be returned
请注意,与Option enum一样,Result
枚举及其成员也通过prelude进入了作用域,因此我们不需要在匹配分支中的Ok和Err成员之前指定Result::
。
当结果为Ok
时,这段代码将返回Ok
中的file
,然后我们将该文件句柄值赋给变量greeting_file
。在match
之后,我们可以使用文件句柄进行读写了。match
的另外一个分支就会从File::open
中得到一个Err
的值。在这个示例中,我们现在调用panic!
宏(macro)。如果没有一个叫hello.txt
的文件在当前文件夹,并且运行了这段代码,我们就会看到如下来自panic!
宏的错误输出:
1 | $ cargo run |
像往常一样,这个输出告诉我们哪里出了问题。
匹配不同的错误(Matching on Different Errors)
Listing9-4无论File::open
因为什么失败都会报pannic!
错误。然而,我们希望针对不同的失败原因采取不同的行动:如果File::open
失败是因为文件不存在,我们想创建一个文件并返回新文件的句柄。如果File::open
失败是因为其他原因–比如,因为我们没有打开文件的权限–我们仍然和Listing9-4一样panic!
。为此,我们在match
内添加一个内部表达式,如Listing 9-5所示。
Filename: src/main.rs
1 | use std::fs:File; |
Listing 9-5: Handling different kinds of errors in different ways
File::open
内部返回的Err
的值io::Error
,它是标准库提供的数据结构。这个数据结构有一个kind
方法可以得到一个io::ErrorKind
的值。枚举io::ErrorKind
是标准库提供的,并且有不同类型的错误都对应着相应的io操作。我们使用的ErrorKing::NotFound
枚举成员表明我们尝试打开一个不存在的文件。所以我们在greeting_file_result
上匹配,但我们也在error.kind()上进行内部匹配。
我们希望在内部匹配中检查的条件是error.kind()
返回的值是否为ErrorKind
枚举的NotFound
成员。如果是我,我们将尝试通过File::create
创建文件,然而我们创建文件也有可能失败,在match
内部我们需要第二个分支来处理。当文件不能创建,一个不同的错误就会被打印。match
外部保持不变。因此,除文件不存在的情况之外都会报错。
失败时 panic 的简写: unwrap 和 expect
match
已经很好用了,不过它可能有点冗长并且不总是能很好的表明其意图。Result<T, E>
类型定义了很多辅助方法来处理各种情况。其中之一叫做 unwrap
,它的实现就类似于Listing 9-4 中的 match
语句。如果 Result
值是成员 Ok
,unwrap
会返回 Ok
中的值。如果 Result
是成员 Err
,unwrap
会为我们调用 panic!
。这里是一个实践 unwrap
的例子:
1 | use std::fs:File; |
如果我们运行这段代码,并且hello.txt
文件不存在,我们就会看到一个错误信息来自unwrap
方法调用了panic!
:
1 | thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { |
同样,expect
方法也可以自定义panic!
的错误信息。使用expect
而不是unwrap
并提供友好的错误信息可以传达您的意图,并使跟踪panic!
的来源更容易。expect
的语法如下所示:
Filename: src/main.rs
1 | use std::fs:File; |
我们可以和使用unwrap
一样使用expect
来返回一个文件句柄或者调用panic!
。错误信息通过expect
调用panic!
时传递,而不是panic!
默认的错误信息,展示如下:
1 | thread 'main' panicked at 'hello.txt should be included in this project: Os { |
传递错误(Propagating Errors)
当编写一个其实先会调用一些可能会失败的操作的函数时,除了在这个函数中处理错误外,还可以选择让调用者知道这个错误并决定该如何处理。这被称为 传播(propagating)错误,这样能更好的控制代码调用,因为比起你代码所拥有的上下文,调用者可能拥有更多信息或逻辑来决定应该如何处理错误。
例如:Listing 9-6所示,一个函数读一个文件。如果文件不存在或者不能读,函数就会返回一些错误。
Filename: src/main.rs
1 | use std::fs::File; |
Listing 9-6: A function that returns errors to the calling code using match
这个函数可以用更短的方式编写,但我们将从手动做很多事情开始,以探索错误处理;最后,我们将展示较短的方法。我们先看一下函数的返回类型: Result<String, io::Error>
。这意味着该函数返回一个类型的 Result<T, E>
值,其中泛型参数 T
已用具体类型填充,泛型类型已用具体类型 String
填充,E
用io::Error
填充。
如果此函数成功且没有任何问题,则调用此函数的代码将收到一个Ok
值,该值包含String
此函数从文件中读取的username。如果此函数遇到任何问题,调用代码将收到一个 Err
值,该值包含包含有关问题所在的详细信息的实例 io::Error
。我们选择 io::Error
此函数的返回类型,因为这恰好是我们在此函数主体中调用的两个操作返回的错误值的类型: File::open
函数和 read_to_string
方法。
函数的主体从调用 File::open
函数开始。然后我们用类似于Listing 9-4 match
中的值来处理 match
该 Result
值。如果成功,模式 File::open
变量中的文件句柄将成为可变变量 username_file
中的值,函数将继续。在这种情况下 Err
,我们不是调用,而是使用 return
关键字提前完全返回函数,并从File::open
返回的错误传递出去,模式匹配中的e
作为该函数的错误值传递回调用 panic! 代码。
因此,如果我们在username_file
中有一个文件句柄,那么该函数将在变量username
中创建一个新的String
,并调用username_file
中文件句柄上的read_to_string
方法来将文件的内容读入username
。read_to_string
方法也返回Result
,因为它可能失败,即使File::open
打开文件成功,读文件也有可能失败。所以我们需要另一个匹配来处理这个Result
:如果read_to_string
成功,那么我们的函数就成功了,我们从文件中返回username,这个username现在被Ok
封装在username
中。如果read_to_string
操作失败,返回错误值的方式与处理File::open
返回值的匹配中返回错误值的方式相同。然而,我们不需要显式return
,因为这是函数中的最后一个表达式。
然后,调用该代码的代码将处理获取包含username
的Ok
值或包含io::Error
的Err
值。由调用代码决定如何处理这些值。如果调用代码得到一个Err
值,它可能会调用panic!
并使程序崩溃,使用默认username,或者从文件以外的其他地方查找username。我们没有足够的信息来了解调用代码实际尝试做什么,所以我们向上传播所有成功或错误信息,以便它正确处理。
这种传播错误的模式在Rust中非常常见,因此Rust为了方便起见提供了问号操作符?
。
传播错误的快捷方式:?
操作符
如Listing9-7所示,实现了一个和Listing9-6相同的read_username_from_file
的函数,但是使用了?
操作来实现。
Filename: src/main.rs
1 | use std::fs::File; |
Listing 9-7: A function that returns errors to the calling code using the `?`` operator
?
操作符放置在Result
值之后,其工作方式与Listing 9-6中为处理Result
值而定义的匹配表达式几乎相同。如果Result
的值为Ok
,则该表达式将返回Ok
中的值,程序将继续执行。如果该值为Err
,则整个函数将返回Err
,就像我们使用了return
关键字一样,因此错误值将传播到调用代码。
Listing 9-6中的匹配表达式的作用与?
操作符所做的事情:错误值有?
在标准库中的From
trait中定义了from函数,该函数用于将值从一种类型转换为另一种类型。什么时候?
操作符调用from
函数,接收到的错误类型被转换为当前函数返回类型中定义的错误类型。当函数返回一种错误类型来表示函数可能失败的所有方式时,即使部分可能因许多不同的方式而失败,这也是有用的。
例如,我们可以修改Listing 9-7中的read_username_from_file
函数,使其返回一个自定义的错误类型OurError
。如果我们还为OurError
定义impl From<io::Error>
,从io::Error
构造OurError
的实例,那么?
操作符会在read_username_from_file
函数体中将调用from
并转换错误类型,而无需向函数中添加任何代码。
在Listing 9-7的上下文中,?
在File::open
调用的末尾将把Ok
中的值返回给变量username_file
。如果发生错误,?
操作符将提前返回,并向调用代码提供任何Err
值。同样的道理也适用于?
在read_to_string
调用结束时。
?
操作符消除了大量的模板代码,使这个函数的实现更简单。我们甚至可以通过在?
之后立即连接方法调用来进一步缩短代码,如Listing 9-8所示。
Filename: src/main.rs
1 | use std::fs::File; |
Listing 9-8: Chaining method calls after the ?
operator
我们将username
中String
的创建移到了函数的开头;这一点没有改变。我们没有创建一个可变的用户名文件,而是将read_to_string
调用直接连接到file::open("hello.txt")
的结果上。我们还有?
当File::open
和read_to_string
都成功时,我们仍然返回一个包含username
的Ok
值,而不是返回错误。功能与Listing 9-6和Listing 9-7相同;这是一种不同的,更符合工程学的写法。
Listing 9-9 所示使用了fs::read_to_string
将使代码更加简短。
1 | use std::fs; |
Listing 9-9: Using fs::read_to_string
instead of opening and then reading the file
将文件读入字符串是一种相当常见的操作,因此标准库提供了方便的fs::read_to_string
函数,该函数打开文件,创建一个新的String
,读取文件的内容,将内容放入该String
,并返回它。当然,使用fs::read_to_string
并不能让我们有机会解释所有的错误处理,所以我们先用更长的方法来解释。
哪里可以使用?
操作
?
操作符只能用于返回类型与?
操作符兼容的函数中。这是因为?
操作符的定义是执行从函数中提前返回一个值,方式与Listing 9-6中定义的match
表达式相同。在Listing 9-6中,匹配使用一个Result
值,而提前返回臂返回一个Err(e)
值。函数的返回类型必须是Result
,以便与此返回兼容。
在Listing 9-10中,让我们看看如果使用?
返回类型与我们使用的值的类型不兼容的主函数中的操作符?
:
Filename: src/main.rs
1 | use std::fs::File; |
Listing 9-10: Attempting to use the ?
in the main
function that returns `()`` won’t compile
这段代码打开一个文件,可能会失败。?
操作符在File::open
返回的Result
值之后,但是这个主函数的返回类型是()
,而不是Result
。当我们编译这段代码时,会得到以下错误消息:
1 | cargo run |
这个错误指出我们只允许使用?
返回Result
、Option
或其他实现FromResidual
的类型的函数中的操作符。
要修复这个错误,您有两种选择。一种选择是更改函数的返回类型,使其与使用的值兼容。只要没有限制,就继续操作。另一种技术是使用match
或Result<T, E>
方法之一,以任何合适的方式处理Result<T, E>
。
错误信息中还提到?
也可以与Option<T>
值一起使用。就像使用?
在Result
中,您只能使用?
在返回一个Option
的函数中使用Option
。?
操作符在Option<T>
上调用时的行为与在Result<T, E>
上调用时的行为相似:如果值为None
,则在该点将提前从函数返回None
。如果值是Some
,则Some
中的值是表达式的结果值,函数继续执行。Listing 9-11给出了一个函数示例,该函数查找给定格式中第一行的最后一个字符:
1 | fn last_char_of_first_line(text: &str) -> Option<char> { |
Listing 9-11: Using the ?
operator on an Option<T>
value
这个函数返回Option<char>
,因为有可能有字符,但也有可能没有。这段代码接受text
字符串切片参数并对其调用lines
方法,该方法返回一个遍历字符串中的行的迭代器。因为这个函数想要检查第一行,所以它在迭代器上调用next
以从迭代器中获取第一个值。如果text
是空字符串,对next
的调用将返回None
,在这种情况下我们使用?
停止并从第一行的最后一个字符返回None
。如果text
不是空字符串,next
将返回一个Some
值, 其中包含text
中第一行的字符串切片。
?
操作符提取字符串切片,然后调用该字符串切片上的chars
来获取其字符的迭代器。我们对第一行的最后一个字符感兴趣,因此调用last
来返回迭代器中的最后一项。这是一个选项,因为第一行可能是空字符串,例如,如果文本以空行开头,但在其他行上有字符,如“\nhi”
。但是,如果第一行有最后一个字符,它将在Some
变体中返回。?
运算符在中间给了我们一种简洁的方式来表达这个逻辑,允许我们实现
注意,您可以使用?
操作符对返回Result
的函数中的Result
进行操作,您可以使用?操作符在返回Option
的函数中对Option
进行操作,但不能混合匹配。?
操作符不会自动将Result
转换为Option
,反之亦然;在这些情况下,您可以使用诸如Result
上的ok
方法或Option
上的ok_or
方法来显式地进行转换。
到目前为止,我们使用的所有主要函数都是return()
。main
函数的特殊之处在于它是可执行程序的入口和出口点,它的返回类型是有限制的,这样程序才能按照预期的方式运行。
幸运的是,main
也可以返回Result<(), E>
。Listing 9-12拥有Listing 9-10的代码,但我们将main
的返回类型更改为Result<()
, Box<dyn Error>>
,并在末尾添加返回值Ok(())
。这段代码现在可以编译了:
1 | use std::error::Error; |
Listing 9-12: Changing main to return Result<(), E>
allows the use of the ?
operator on Result
values
Box<dyn Error>
类型是一个trait
对象,我们将在第17章使用允许不同类型值的trait
对象一节中讨论它。现在,您可以读取Box<dyn Error>
来表示任何类型的错误。使用?
允许在错误类型为Box<dyn error >
的主函数中返回Result
值,因为它允许提前返回任何Err
值。即使这个主函数的主体只会返回std::io::Error
类型的错误,通过指定Box<dyn Error>
,即使将返回其他错误的更多代码添加到main的主体中,该签名仍然是正确的。
当main
函数返回Result<(), E>
时,如果main
函数返回Ok(())
,可执行程序将以0
的值退出;如果main
函数返回Err
值,可执行程序将以非0
的值退出。用C编写的可执行程序在退出时返回整数:成功退出的程序返回整数0
,出错的程序返回非0的整数。Rust还从可执行文件返回整数,以与此约定兼容。
main函数可以返回任何实现std::process::Termination
trait的类型,它包含一个可以返回ExitCode
的函数report
。有关为您自己的类型实现Termination
特性的更多信息,请参阅标准库文档。
到现在,我们已经详细讨论了调用panic!
或者返回Result
。