ch09-02-recoverable-errors-with-result.md
commit 0bac27c66136764c82fe267763945f3c65eea002
大部分錯誤并沒有嚴重到需要程序完全停止執(zhí)行。有時,一個函數(shù)會因為一個容易理解并做出反應(yīng)的原因失敗。例如,如果因為打開一個并不存在的文件而失敗,此時我們可能想要創(chuàng)建這個文件,而不是終止進程。
回憶一下第二章 “使用 ?Result
?類型來處理潛在的錯誤” 部分中的那個 Result
枚舉,它定義有如下兩個成員,Ok
和 Err
:
enum Result<T, E> {
Ok(T),
Err(E),
}
T
和 E
是泛型類型參數(shù);第十章會詳細介紹泛型?,F(xiàn)在你需要知道的就是 T
代表成功時返回的 Ok
成員中的數(shù)據(jù)的類型,而 E
代表失敗時返回的 Err
成員中的錯誤的類型。因為 Result
有這些泛型類型參數(shù),我們可以將 Result
類型和標準庫中為其定義的函數(shù)用于很多不同的場景,這些情況中需要返回的成功值和失敗值可能會各不相同。
讓我們調(diào)用一個返回 Result
的函數(shù),因為它可能會失?。喝缡纠?9-3 所示打開一個文件:
文件名: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
}
示例 9-3:打開文件
如何知道 File::open
返回一個 Result
呢?我們可以查看 標準庫 API 文檔,或者可以直接問編譯器!如果給 f
某個我們知道 不是 函數(shù)返回值類型的類型注解,接著嘗試編譯代碼,編譯器會告訴我們類型不匹配。然后錯誤信息會告訴我們 f
的類型 應(yīng)該 是什么。讓我們試試!我們知道 File::open
的返回值不是 u32
類型的,所以將 let f
語句改為如下:
let f: u32 = File::open("hello.txt");
現(xiàn)在嘗試編譯會給出如下輸出:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| --- ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
| |
| expected due to this
|
= note: expected type `u32`
found enum `Result<File, std::io::Error>`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `error-handling` due to previous error
這就告訴我們了 File::open
函數(shù)的返回值類型是 Result<T, E>
。這里泛型參數(shù) T
放入了成功值的類型 std::fs::File
,它是一個文件句柄。E
被用在失敗值上時 E
的類型是 std::io::Error
。
這個返回值類型說明 File::open
調(diào)用可能會成功并返回一個可以進行讀寫的文件句柄。這個函數(shù)也可能會失敗:例如,文件可能并不存在,或者可能沒有訪問文件的權(quán)限。File::open
需要一個方式告訴我們是成功還是失敗,并同時提供給我們文件句柄或錯誤信息。而這些信息正是 Result
枚舉可以提供的。
當 File::open
成功的情況下,變量 f
的值將會是一個包含文件句柄的 Ok
實例。在失敗的情況下,f
的值會是一個包含更多關(guān)于出現(xiàn)了何種錯誤信息的 Err
實例。
我們需要在示例 9-3 的代碼中增加根據(jù) File::open
返回值進行不同處理的邏輯。示例 9-4 展示了一個使用基本工具處理 Result
的例子:第六章學(xué)習(xí)過的 match
表達式。
文件名: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {:?}", error),
};
}
示例 9-4:使用 match
表達式處理可能會返回的 Result
成員
注意與 Option
枚舉一樣,Result
枚舉和其成員也被導(dǎo)入到了 prelude 中,所以就不需要在 match
分支中的 Ok
和 Err
之前指定 Result::
。
這里我們告訴 Rust 當結(jié)果是 Ok
時,返回 Ok
成員中的 file
值,然后將這個文件句柄賦值給變量 f
。match
之后,我們可以利用這個文件句柄來進行讀寫。
match
的另一個分支處理從 File::open
得到 Err
值的情況。在這種情況下,我們選擇調(diào)用 panic!
宏。如果當前目錄沒有一個叫做 hello.txt 的文件,當運行這段代碼時會看到如下來自 panic!
宏的輸出:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished dev [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
一如既往,此輸出準確地告訴了我們到底出了什么錯。
示例 9-4 中的代碼不管 File::open
是因為什么原因失敗都會 panic!
。我們真正希望的是對不同的錯誤原因采取不同的行為:如果 File::open
因為文件不存在而失敗,我們希望創(chuàng)建這個文件并返回新文件的句柄。如果 File::open
因為任何其他原因失敗,例如沒有打開文件的權(quán)限,我們?nèi)匀幌M袷纠?9-4
那樣 panic!
。讓我們看看示例 9-5,其中 match
增加了另一個分支:
文件名: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => {
panic!("Problem opening the file: {:?}", other_error)
}
},
};
}
示例 9-5:使用不同的方式處理不同類型的錯誤
File::open
返回的 Err
成員中的值類型 io::Error
,它是一個標準庫中提供的結(jié)構(gòu)體。這個結(jié)構(gòu)體有一個返回 io::ErrorKind
值的 kind
方法可供調(diào)用。io::ErrorKind
是一個標準庫提供的枚舉,它的成員對應(yīng) io
操作可能導(dǎo)致的不同錯誤類型。我們感興趣的成員是 ErrorKind::NotFound
,它代表嘗試打開的文件并不存在。這樣,match
就匹配完 f
了,不過對于 error.kind()
還有一個內(nèi)層 match
。
我們希望在內(nèi)層 match
中檢查的條件是 error.kind()
的返回值是否為 ErrorKind
的 NotFound
成員。如果是,則嘗試通過 File::create
創(chuàng)建文件。然而因為 File::create
也可能會失敗,還需要增加一個內(nèi)層 match
語句。當文件不能被打開,會打印出一個不同的錯誤信息。外層 match
的最后一個分支保持不變,這樣對任何除了文件不存在的錯誤會使程序
panic。
不同于使用 ?
match
?和 ?Result<T, E>
?這里有好多
match
!match
確實很強大,不過也非常的基礎(chǔ)。第十三章我們會介紹閉包(closure),這可以用于很多Result<T, E>
上定義的方法。在處理代碼中的Result<T, E>
值時這些方法可能會更加簡潔。
例如,這是另一個編寫與示例 9-5 邏輯相同但是使用閉包和
unwrap_or_else
方法的例子:
use std::fs::File; use std::io::ErrorKind; fn main() { let f = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {:?}", error); }) } else { panic!("Problem opening the file: {:?}", error); } }); }
雖然這段代碼有著如示例 9-5 一樣的行為,但并沒有包含任何
match
表達式且更容易閱讀。在閱讀完第十三章后再回到這個例子,并查看標準庫文檔unwrap_or_else
方法都做了什么操作。在處理錯誤時,還有很多這類方法可以消除大量嵌套的match
表達式。
match
能夠勝任它的工作,不過它可能有點冗長并且不總是能很好的表明其意圖。Result<T, E>
類型定義了很多輔助方法來處理各種情況。其中之一叫做 unwrap
,它的實現(xiàn)就類似于示例 9-4 中的 match
語句。如果 Result
值是成員 Ok
,unwrap
會返回 Ok
中的值。如果 Result
是成員 Err
,unwrap
會為我們調(diào)用 panic!
。這里是一個實踐 unwrap
的例子:
文件名: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
如果調(diào)用這段代碼時不存在 hello.txt 文件,我們將會看到一個 unwrap
調(diào)用 panic!
時提供的錯誤信息:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4
還有另一個類似于 unwrap
的方法它還允許我們選擇 panic!
的錯誤信息:expect
。使用 expect
而不是 unwrap
并提供一個好的錯誤信息可以表明你的意圖并更易于追蹤 panic 的根源。expect
的語法看起來像這樣:
文件名: src/main.rs
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
expect
與 unwrap
的使用方式一樣:返回文件句柄或調(diào)用 panic!
宏。expect
在調(diào)用 panic!
時使用的錯誤信息將是我們傳遞給 expect
的參數(shù),而不像 unwrap
那樣使用默認的 panic!
信息。它看起來像這樣:
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
因為這個錯誤信息以我們指定的文本開始,Failed to open hello.txt
,將會更容易找到代碼中的錯誤信息來自何處。如果在多處使用 unwrap
,則需要花更多的時間來分析到底是哪一個 unwrap
造成了 panic,因為所有的 unwrap
調(diào)用都打印相同的信息。
當編寫一個其實先會調(diào)用一些可能會失敗的操作的函數(shù)時,除了在這個函數(shù)中處理錯誤外,還可以選擇讓調(diào)用者知道這個錯誤并決定該如何處理。這被稱為 傳播(propagating)錯誤,這樣能更好的控制代碼調(diào)用,因為比起你代碼所擁有的上下文,調(diào)用者可能擁有更多信息或邏輯來決定應(yīng)該如何處理錯誤。
例如,示例 9-6 展示了一個從文件中讀取用戶名的函數(shù)。如果文件不存在或不能讀取,這個函數(shù)會將這些錯誤返回給調(diào)用它的代碼:
文件名: src/main.rs
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
示例 9-6:一個函數(shù)使用 match
將錯誤返回給代碼調(diào)用者
首先讓我們看看函數(shù)的返回值:Result<String, io::Error>
。這意味著函數(shù)返回一個 Result<T, E>
類型的值,其中泛型參數(shù) T
的具體類型是 String
,而 E
的具體類型是 io::Error
。如果這個函數(shù)沒有出任何錯誤成功返回,函數(shù)的調(diào)用者會收到一個包含 String
的 Ok
值 —— 函數(shù)從文件中讀取到的用戶名。如果函數(shù)遇到任何錯誤,函數(shù)的調(diào)用者會收到一個 Err
值,它儲存了一個包含更多這個問題相關(guān)信息的 io::Error
實例。這里選擇 io::Error
作為函數(shù)的返回值是因為它正好是函數(shù)體中那兩個可能會失敗的操作的錯誤返回值:File::open
函數(shù)和 read_to_string
方法。
函數(shù)體以調(diào)用 File::open
函數(shù)開始。接著使用 match
處理返回值 Result
,類似示例 9-4,如果 File::open
成功了,模式變量 file
中的文件句柄就變成了可變變量 f
中的值,接著函數(shù)繼續(xù)執(zhí)行。在 Err
的情況下,我們沒有調(diào)用 panic!
,而是使用 return
關(guān)鍵字提前結(jié)束整個函數(shù),并將來自 File::open
的錯誤值(現(xiàn)在在模式變量 e
中)作為函數(shù)的錯誤值傳回給調(diào)用者。
所以 f
中有了一個文件句柄,函數(shù)接著在變量 s
中創(chuàng)建了一個新 String
并調(diào)用文件句柄 f
的 read_to_string
方法來將文件的內(nèi)容讀取到 s
中。read_to_string
方法也返回一個 Result
因為它也可能會失?。耗呐率?nbsp;File::open
已經(jīng)成功了。所以我們需要另一個 match
來處理這個 Result
:如果 read_to_string
成功了,那么這個函數(shù)就成功了,并返回文件中的用戶名,它現(xiàn)在位于被封裝進 Ok
的 s
中。如果read_to_string
失敗了,則像之前處理 File::open
的返回值的 match
那樣返回錯誤值。不過并不需要顯式的調(diào)用 return
,因為這是函數(shù)的最后一個表達式。
調(diào)用這個函數(shù)的代碼最終會得到一個包含用戶名的 Ok
值,或者一個包含 io::Error
的 Err
值。我們無從得知調(diào)用者會如何處理這些值。例如,如果他們得到了一個 Err
值,他們可能會選擇 panic!
并使程序崩潰、使用一個默認的用戶名或者從文件之外的地方尋找用戶名。我們沒有足夠的信息知曉調(diào)用者具體會如何嘗試,所以將所有的成功或失敗信息向上傳播,讓他們選擇合適的處理方法。
這種傳播錯誤的模式在 Rust 是如此的常見,以至于 Rust 提供了 ?
問號運算符來使其更易于處理。
示例 9-7 展示了一個 read_username_from_file
的實現(xiàn),它實現(xiàn)了與示例 9-6 中的代碼相同的功能,不過這個實現(xiàn)使用了 ?
運算符:
文件名: src/main.rs
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
示例 9-7:一個使用 ?
運算符向調(diào)用者返回錯誤的函數(shù)
Result
值之后的 ?
被定義為與示例 9-6 中定義的處理 Result
值的 match
表達式有著完全相同的工作方式。如果 Result
的值是 Ok
,這個表達式將會返回 Ok
中的值而程序?qū)⒗^續(xù)執(zhí)行。如果值是 Err
,Err
中的值將作為整個函數(shù)的返回值,就好像使用了 return
關(guān)鍵字一樣,這樣錯誤值就被傳播給了調(diào)用者。
示例 9-6 中的 match
表達式與問號運算符所做的有一點不同:?
運算符所使用的錯誤值被傳遞給了 from
函數(shù),它定義于標準庫的 From
trait 中,其用來將錯誤從一種類型轉(zhuǎn)換為另一種類型。當 ?
運算符調(diào)用 from
函數(shù)時,收到的錯誤類型被轉(zhuǎn)換為由當前函數(shù)返回類型所指定的錯誤類型。這在當函數(shù)返回單個錯誤類型來代表所有可能失敗的方式時很有用,即使其可能會因很多種原因失敗。只要每一個錯誤類型都實現(xiàn)了 from
函數(shù)來定義如何將自身轉(zhuǎn)換為返回的錯誤類型,?
運算符會自動處理這些轉(zhuǎn)換。
在示例 9-7 的上下文中,File::open
調(diào)用結(jié)尾的 ?
將會把 Ok
中的值返回給變量 f
。如果出現(xiàn)了錯誤,?
運算符會提早返回整個函數(shù)并將一些 Err
值傳播給調(diào)用者。同理也適用于 read_to_string
調(diào)用結(jié)尾的 ?
。
?
運算符消除了大量樣板代碼并使得函數(shù)的實現(xiàn)更簡單。我們甚至可以在 ?
之后直接使用鏈式方法調(diào)用來進一步縮短代碼,如示例 9-8 所示:
文件名: src/main.rs
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
示例 9-8:問號運算符之后的鏈式方法調(diào)用
在 s
中創(chuàng)建新的 String
被放到了函數(shù)開頭;這一部分沒有變化。我們對 File::open("hello.txt")?
的結(jié)果直接鏈式調(diào)用了 read_to_string
,而不再創(chuàng)建變量 f
。仍然需要 read_to_string
調(diào)用結(jié)尾的 ?
,而且當 File::open
和 read_to_string
都成功沒有失敗時返回包含用戶名 s
的 Ok
值。其功能再一次與示例 9-6 和示例 9-7 保持一致,不過這是一個與眾不同且更符合工程學(xué)(ergonomic)的寫法。
說到編寫這個函數(shù)的不同方法,甚至還有一個更短的寫法:
文件名: src/main.rs
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
示例 9-9: 使用 fs::read_to_string
將文件讀取到一個字符串是相當常見的操作,所以 Rust 提供了名為 fs::read_to_string
的函數(shù),它會打開文件、新建一個 String
、讀取文件的內(nèi)容,并將內(nèi)容放入 String
,接著返回它。當然,這樣做就沒有展示所有這些錯誤處理的機會了,所以我們最初就選擇了艱苦的道路。
?
運算符只能被用于返回值與 ?
作用的值相兼容的函數(shù)。因為 ?
運算符被定義為從函數(shù)中提早返回一個值,這與示例 9-6 中的 match
表達式有著完全相同的工作方式。示例 9-6 中 match
作用于一個 Result
值,提早返回的分支返回了一個 Err(e)
值。函數(shù)的返回值必須是 Result
才能與這個 return
相兼容。
在示例 9-10 中,讓我們看看在返回值不兼容的 main
函數(shù)中使用 ?
運算符會得到什么錯誤:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
示例 9-10: 嘗試在返回 ()
的 main
函數(shù)中使用 ?
的代碼不能編譯
這段代碼打開一個文件,這可能會失敗。?
運算符作用于 File::open
返回的 Result
值,不過 main
函數(shù)的返回類型是 ()
而不是 Result
。當編譯這些代碼,會得到如下錯誤信息:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:36
|
3 | / fn main() {
4 | | let f = File::open("hello.txt")?;
| | ^ cannot use the `?` operator in a function that returns `()`
5 | | }
| |_- this function should return `Result` or `Option` to accept `?`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error
這個錯誤指出只能在返回 Result
或者其它實現(xiàn)了 FromResidual
的類型的函數(shù)中使用 ?
運算符。為了修復(fù)這個錯誤,有兩個選擇。一個是,如果沒有限制的話將函數(shù)的返回值改為 Result<T, E>
。另一個是使用 match
或 Result<T, E>
的方法中合適的一個來處理 Result<T, E>
。
錯誤信息也提到 ?
也可用于 Option<T>
值。如同對 Result
使用 ?
一樣,只能在返回 Option
的函數(shù)中對 Option
使用 ?
。在 Option<T>
上調(diào)用 ?
運算符的行為與 Result<T, E>
類似:如果值是 None
,此時 None
會從函數(shù)中提前返回。如果值是 Some
,Some
中的值作為表達式的返回值同時函數(shù)繼續(xù)。示例 9-11 中有一個從給定文本中返回第一行最后一個字符的函數(shù)的例子:
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
示例 9-11: 在 Option<T>
值上使用 ?
運算符
這個函數(shù)返回 Option<char>
因為它可能會在這個位置找到一個字符,也可能沒有字符。這段代碼獲取 text
字符串 slice 作為參數(shù)并調(diào)用其 lines
方法,這會返回一個字符串中每一行的迭代器。因為函數(shù)希望檢查第一行,所以調(diào)用了迭代器 next
來獲取迭代器中第一個值。如果 text
是空字符串,next
調(diào)用會返回 None
,此時我們可以使用 ?
來停止并從 last_char_of_first_line
返回 None
。如果 text
不是空字符串,next
會返回一個包含 text
中第一行的字符串 slice 的 Some
值。
?
會提取這個字符串 slice,然后可以在字符串 slice 上調(diào)用 chars
來獲取字符的迭代器。我們感興趣的是第一行的最后一個字符,所以可以調(diào)用 last
來返回迭代器的最后一項。這是一個 Option
,因為有可能第一行是一個空字符串,例如 text
以一個空行開頭而后面的行有文本,像是 "\nhi"
。不過,如果第一行有最后一個字符,它會返回在一個 Some
成員中。?
運算符作用于其中給了我們一個簡潔的表達這種邏輯的方式。如果我們不能在 Option
上使用 ?
運算符,則不得不使用更多的方法調(diào)用或者 match
表達式來實現(xiàn)這些邏輯。
注意你可以在返回 Result
的函數(shù)中對 Result
使用 ?
運算符,可以在返回 Option
的函數(shù)中對 Option
使用 ?
運算符,但是不可以混合搭配。?
運算符不會自動將 Result
轉(zhuǎn)化為 Option
,反之亦然;在這些情況下,可以使用類似 Result
的 ok
方法或者 Option
的 ok_or
方法來顯式轉(zhuǎn)換。
目前為止,我們所使用的所有 main
函數(shù)都返回 ()
。main
函數(shù)是特殊的因為它是可執(zhí)行程序的入口點和退出點,為了使程序能正常工作,其可以返回的類型是有限制的。
幸運的是 main
函數(shù)也可以返回 Result<(), E>
, 示例 9-12 中的代碼來自示例 9-10 不過修改了 main
的返回值為 Result<(), Box<dyn Error>>
并在結(jié)尾增加了一個 Ok(())
作為返回值。這段代碼可以編譯:
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
示例 9-12: 修改 main
返回 Result<(), E>
允許對 Result
值使用 ?
運算符
Box<dyn Error>
類型是一個 trait 對象(trait object)第十七章 “為使用不同類型的值而設(shè)計的 trait 對象” 部分會做介紹。目前可以將 Box<dyn Error>
理解為 “任何類型的錯誤”。在返回 Box<dyn Error>
錯誤類型 main
函數(shù)中對 Result
使用 ?
是允許的,因為它允許任何 Err
值提前返回。
當 main
函數(shù)返回 Result<(), E>
,如果 main
返回 Ok(())
可執(zhí)行程序會以 0
值退出,而如果 main
返回 Err
值則會以非零值退出;成功退出的程序會返回整數(shù) 0
,運行錯誤的程序會返回非 0
的整數(shù)。Rust 也會從二進制程序中返回與這個慣例相兼容的整數(shù)。
main
函數(shù)也可以返回任何實現(xiàn)了 std::process::Termination
trait 的類型。截至本書編寫時,Termination
trait 是一個不穩(wěn)定功能(unstable feature),只能用于 Nightly Rust 中,所以你不能在 穩(wěn)定版 Rust(Stable Rust)中用自己的類型去實現(xiàn),不過有朝一日應(yīng)該可以!
現(xiàn)在我們討論過了調(diào)用 panic!
或返回 Result
的細節(jié),是時候回到他們各自適合哪些場景的話題了。
更多建議: