鍍金池/ 教程/ Java/ 17.錯誤處理
標準輸入與輸出
消息傳遞
循環(huán)
注釋
Rust for Mac OS
幾種智能指針
Cell, RefCell
trait對象 (trait object)
rust web 開發(fā)
Unsafe、原始指針
Macro
迭代器
函數(shù)
Borrow, BorrowMut, ToOwned
快速上手
二叉樹
編輯器
測試與評測
Deref
安裝Rust
哈希表 HashMap
原生類型
17.錯誤處理
VS Code 安裝配置
動態(tài)數(shù)組Vec
模式匹配
操作符和格式化字符串
Rust for Linux
函數(shù)參數(shù)
Visual Studio
vim/GVim安裝配置
閉包作為參數(shù)和返回值
安全(Safety)
Cow
生命周期( Lifetime )
閉包的實現(xiàn)
所有權(quán)(Ownership)
Atom
將Rust編譯成庫
類型、運算符和字符串
類型系統(tǒng)中的幾個常見 trait
特性
屬性和編譯器參數(shù)
Spacemacs
集合類型
Rust json處理
Heap & Stack
并行
標準庫示例
基本程序結(jié)構(gòu)
鏈表
trait 和 trait對象
前期準備
代碼風(fēng)格
編譯器參數(shù)
基于語義化版本的項目版本聲明與管理
Rust 版本管理工具: rustup
引用&借用(References&Borrowing)
注釋與文檔
10.1 trait關(guān)鍵字
模式
調(diào)用ffi函數(shù)
unsafe
并發(fā),并行,多線程編程
AsRef 和 AsMut
Rust旅程
Rust for Windows
結(jié)構(gòu)體與枚舉
條件分支
附錄I-術(shù)語表
變量綁定與原生類型
Mutex 與 RwLock
泛型
裸指針
常用數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
系統(tǒng)命令:調(diào)用grep
Into/From 及其在 String 和 &str 互轉(zhuǎn)上的應(yīng)用
共享內(nèi)存
Sublime
網(wǎng)絡(luò)模塊:W貓的回音
函數(shù)返回值
包和模塊
高階函數(shù)
函數(shù)與方法
match關(guān)鍵字
隊列
目錄操作:簡單grep
語句和表達式
并發(fā)編程
閉包
測試
閉包的語法
同步
迭代器
String
Send 和 Sync
Rc 和 Arc
屬性
Emacs
優(yōu)先隊列
Prelude
cargo簡介
控制流(control flow)
數(shù)組、動態(tài)數(shù)組和字符串
FFI
模塊和包系統(tǒng)、Prelude
實戰(zhàn)篇
Rust 是一門系統(tǒng)級編程語言,被設(shè)計為保證內(nèi)存和線程安全,并防止段錯誤。作為系統(tǒng)級編程語言,它的基本理念是 “零開銷抽象”。理
運算符重載
Any和反射
rust數(shù)據(jù)庫操作
輸入輸出流
復(fù)合類型
性能測試

17.錯誤處理

錯誤處理是保證程序健壯性的前提,在編程語言中錯誤處理的方式大致分為兩種:拋出異常(exceptions)和作為值返回。

Rust 將錯誤作為值返回并且提供了原生的優(yōu)雅的錯誤處理方案。

熟練掌握錯誤處理是軟件工程中非常重要的環(huán)節(jié),讓我一起來看看Rust展現(xiàn)給我們的錯誤處理藝術(shù)。

17.1 Option和Result

謹慎使用panic

fn guess(n: i32) -> bool {
    if n < 1 || n > 10 {
        panic!("Invalid number: {}", n);
    }
    n == 5
}

fn main() {
    guess(11);
}

panic會導(dǎo)致當前線程結(jié)束,甚至是整個程序的結(jié)束,這往往是不被期望看到的結(jié)果。(編寫示例或者簡短代碼的時候panic不失為一個好的建議)

Option

enum Option<T> {
    None,
    Some(T),
}

Option 是Rust的系統(tǒng)類型,用來表示值不存在的可能,這在編程中是一個好的實踐,它強制Rust檢測和處理值不存在的情況。例如:

fn find(haystack: &str, needle: char) -> Option<usize> {
    for (offset, c) in haystack.char_indices() {
        if c == needle {
            return Some(offset);
        }
    }
    None
}

find在字符串haystack中查找needle字符,事實上結(jié)果會出現(xiàn)兩種可能,有(Some(usize))或無(None)。

fn main() {
    let file_name = "foobar.rs";
    match find(file_name, '.') {
        None => println!("No file extension found."),
        Some(i) => println!("File extension: {}", &file_name[i+1..]),
    }
}

Rust 使用模式匹配來處理返回值,調(diào)用者必須處理結(jié)果為None的情況。這往往是一個好的編程習(xí)慣,可以減少潛在的bug。Option 包含一些方法來簡化模式匹配,畢竟過多的match會使代碼變得臃腫,這也是滋生bug的原因之一。

unwrap

impl<T> Option<T> {
    fn unwrap(self) -> T {
        match self {
            Option::Some(val) => val,
            Option::None =>
              panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

unwrap當遇到None值時會panic,如前面所說這不是一個好的工程實踐。不過有些時候卻非常有用:

  • 在例子和簡單快速的編碼中 有的時候你只是需要一個小例子或者一個簡單的小程序,輸入輸出已經(jīng)確定,你根本沒必要花太多時間考慮錯誤處理,使用unwrap變得非常合適。
  • 當程序遇到了致命的bug,panic是最優(yōu)選擇

map

假如我們要在一個字符串中找到文件的擴展名,比如foo.rs中的rs, 我們可以這樣:

fn extension_explicit(file_name: &str) -> Option<&str> {
    match find(file_name, '.') {
        None => None,
        Some(i) => Some(&file_name[i+1..]),
    }
}

fn main() {
    match extension_explicit("foo.rs") {
        None => println!("no extension"),
        Some(ext) =>  assert_eq!(ext, "rs"),
    }
}

我們可以使用map簡化:

// map是標準庫中的方法
fn map<F, T, A>(option: Option<T>, f: F) -> Option<A> where F: FnOnce(T) -> A {
    match option {
        None => None,
        Some(value) => Some(f(value)),
    }
}
// 使用map去掉match
fn extension(file_name: &str) -> Option<&str> {
    find(file_name, '.').map(|i| &file_name[i+1..])
}

map如果有值Some(T)會執(zhí)行f,反之直接返回None

unwrap_or

fn unwrap_or<T>(option: Option<T>, default: T) -> T {
    match option {
        None => default,
        Some(value) => value,
    }
}

unwrap_or提供了一個默認值default,當值為None時返回default

fn main() {
    assert_eq!(extension("foo.rs").unwrap_or("rs"), "rs");
    assert_eq!(extension("foo").unwrap_or("rs"), "rs");
}

and_then

fn and_then<F, T, A>(option: Option<T>, f: F) -> Option<A>
        where F: FnOnce(T) -> Option<A> {
    match option {
        None => None,
        Some(value) => f(value),
    }
}

看起來and_thenmap差不多,不過map只是把值為Some(t)重新映射了一遍,and_then則會返回另一個Option。如果我們在一個文件路徑中找到它的擴展名,這時候就會變得尤為重要:

use std::path::Path;
fn file_name(file_path: &str) -> Option<&str> {
    let path = Path::new(file_path);
    path.file_name().to_str()
}
fn file_path_ext(file_path: &str) -> Option<&str> {
    file_name(file_path).and_then(extension)
}

Result

enum Result<T, E> {
    Ok(T),
    Err(E),
}

ResultOption的更通用的版本,比起Option結(jié)果為None它解釋了結(jié)果錯誤的原因,所以:

type Option<T> = Result<T, ()>;

這樣的別名是一樣的(()標示空元組,它既是()類型也可以是()值)

unwrap

impl<T, E: ::std::fmt::Debug> Result<T, E> {
    fn unwrap(self) -> T {
        match self {
            Result::Ok(val) => val,
            Result::Err(err) =>
              panic!("called `Result::unwrap()` on an `Err` value: {:?}", err),
        }
    }
}

沒錯和Option一樣,事實上它們擁有很多類似的方法,不同的是,Result包括了錯誤的詳細描述,這對于調(diào)試人員來說,這是友好的。

Result我們從例子開始

fn double_number(number_str: &str) -> i32 {
    2 * number_str.parse::<i32>().unwrap()
}

fn main() {
    let n: i32 = double_number("10");
    assert_eq!(n, 20);
}

double_number從一個字符串中解析出一個i32的數(shù)字并*2,main中調(diào)用看起來沒什么問題,但是如果把"10"換成其他解析不了的字符串程序便會panic

impl str {
    fn parse<F: FromStr>(&self) -> Result<F, F::Err>;
}

parse返回一個Result,但讓我們也可以返回一個Option,畢竟一個字符串要么能解析成一個數(shù)字要么不能,但是Result給我們提供了更多的信息(要么是一個空字符串,一個無效的數(shù)位,太大或太?。@對于使用者是友好的。當你面對一個Option和Result之間的選擇時。如果你可以提供詳細的錯誤信息,那么大概你也應(yīng)該提供。

這里需要理解一下FromStr這個trait:

pub trait FromStr {
    type Err;
    fn from_str(s: &str) -> Result<Self, Self::Err>;
}

impl FromStr for i32 {
    type Err = ParseIntError;
    fn from_str(src: &str) -> Result<i32, ParseIntError> {

    }
}

number_str.parse::<i32>()事實上調(diào)用的是i32FromStr實現(xiàn)。

我們需要改寫這個例子:

use std::num::ParseIntError;

fn double_number(number_str: &str) -> Result<i32, ParseIntError> {
    number_str.parse::<i32>().map(|n| 2 * n)
}

fn main() {
    match double_number("10") {
        Ok(n) => assert_eq!(n, 20),
        Err(err) => println!("Error: {:?}", err),
    }
}

不僅僅是mapResult同樣包含了unwrap_orand_then。也有一些特有的針對錯誤類型的方法map_error_else

Result別名

Rust的標準庫中會經(jīng)常出現(xiàn)Result的別名,用來默認確認其中Ok(T)或者Err(E)的類型,這能減少重復(fù)編碼。比如io::Result

use std::num::ParseIntError;
use std::result;

type Result<T> = result::Result<T, ParseIntError>;

fn double_number(number_str: &str) -> Result<i32> {
    unimplemented!();
}

組合Option和Result

Option的方法ok_or

fn ok_or<T, E>(option: Option<T>, err: E) -> Result<T, E> {
    match option {
        Some(val) => Ok(val),
        None => Err(err),
    }
}

可以在值為None的時候返回一個Result::Err(E),值為Some(T)的時候返回Ok(T),利用它我們可以組合OptionResult

use std::env;

fn double_arg(mut argv: env::Args) -> Result<i32, String> {
    argv.nth(1)
        .ok_or("Please give at least one argument".to_owned())
        .and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string()))
        .map(|n| 2 * n)
}

fn main() {
    match double_arg(env::args()) {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

double_arg將傳入的命令行參數(shù)轉(zhuǎn)化為數(shù)字并翻倍,ok_orOption類型轉(zhuǎn)換成Resultmap_err當值為Err(E)時調(diào)用作為參數(shù)的函數(shù)處理錯誤

復(fù)雜的例子

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    File::open(file_path)
         .map_err(|err| err.to_string())
         .and_then(|mut file| {
              let mut contents = String::new();
              file.read_to_string(&mut contents)
                  .map_err(|err| err.to_string())
                  .map(|_| contents)
         })
         .and_then(|contents| {
              contents.trim().parse::<i32>()
                      .map_err(|err| err.to_string())
         })
         .map(|n| 2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

file_double從文件中讀取內(nèi)容并將其轉(zhuǎn)化成i32類型再翻倍。 這個例子看起來已經(jīng)很復(fù)雜了,它使用了多個組合方法,我們可以使用傳統(tǒng)的matchif let來改寫它:

use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, String> {
    let mut file = match File::open(file_path) {
        Ok(file) => file,
        Err(err) => return Err(err.to_string()),
    };
    let mut contents = String::new();
    if let Err(err) = file.read_to_string(&mut contents) {
        return Err(err.to_string());
    }
    let n: i32 = match contents.trim().parse() {
        Ok(n) => n,
        Err(err) => return Err(err.to_string()),
    };
    Ok(2 * n)
}

fn main() {
    match file_double("foobar") {
        Ok(n) => println!("{}", n),
        Err(err) => println!("Error: {}", err),
    }
}

這兩種方法個人認為都是可以的,依具體情況來取舍。

try!

macro_rules! try {
    ($e:expr) => (match $e {
        Ok(val) => val,
        Err(err) => return Err(::std::convert::From::from(err)),
    });
}

try!事實上就是match Result的封裝,當遇到Err(E)時會提早返回, ::std::convert::From::from(err)可以將不同的錯誤類型返回成最終需要的錯誤類型,因為所有的錯誤都能通過From轉(zhuǎn)化成Box<Error>,所以下面的代碼是正確的:

use std::error::Error;
use std::fs::File;
use std::io::Read;
use std::path::Path;

fn file_double<P: AsRef<Path>>(file_path: P) -> Result<i32, Box<Error>> {
    let mut file = try!(File::open(file_path));
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents));
    let n = try!(contents.trim().parse::<i32>());
    Ok(2 * n)
}

組合自定義錯誤類型

use std::fs::File;
use std::io::{self, Read};
use std::num;
use std::io;
use std::path::Path;

// We derive `Debug` because all types should probably derive `Debug`.
// This gives us a reasonable human readable description of `CliError` values.
#[derive(Debug)]
enum CliError {
    Io(io::Error),
    Parse(num::ParseIntError),
}

impl From<io::Error> for CliError {
    fn from(err: io::Error) -> CliError {
        CliError::Io(err)
    }
}

impl From<num::ParseIntError> for CliError {
    fn from(err: num::ParseIntError) -> CliError {
        CliError::Parse(err)
    }
}

fn file_double_verbose<P: AsRef<Path>>(file_path: P) -> Result<i32, CliError> {
    let mut file = try!(File::open(file_path).map_err(CliError::Io));
    let mut contents = String::new();
    try!(file.read_to_string(&mut contents).map_err(CliError::Io));
    let n: i32 = try!(contents.trim().parse().map_err(CliError::Parse));
    Ok(2 * n)
}

CliError分別為io::Errornum::ParseIntError實現(xiàn)了From這個trait,所有調(diào)用try!的時候這兩種錯誤類型都能轉(zhuǎn)化成CliError

總結(jié)

熟練使用OptionResult是編寫 Rust 代碼的關(guān)鍵,Rust 優(yōu)雅的錯誤處理離不開值返回的錯誤形式,編寫代碼時提供給使用者詳細的錯誤信息是值得推崇的。

上一篇:模式匹配下一篇:共享內(nèi)存