Rust 語法梳理與總結(下)

楔子

最後我們來複習一下 Rust 的模塊與錯誤處理,等把這兩部分說完之後,我們就可以繼續學習後面的內容了。

先來看看模塊。

模塊

Rust 提供了一套強大的模塊(module)系統,可以將代碼按層次劃分成多個邏輯單元(模塊),並管理這些模塊內部條目的可見性。所以模塊就是條目的集合,而條目可以是:函數、結構體、trait、impl 塊、變量、常量等等,甚至也可以是其它模塊。

我們可以使用 mod 關鍵字定義一個模塊,默認情況下,模塊內的條目都是私有的,除非我們使用 pub 關鍵字將其變成公有。只不過結構體特殊,結構體開頭加 pub 只表示結構體是公有的,但字段還是私有的,如果希望字段也公有,那麼還要在每個字段前面加上 pub。

// 定義一個名爲 a 的模塊
mod a {
    fn func1() {
        println!("我是私有函數 func1");
    }
    pub fn func2() {
        println!("我是公有函數 func2");
    }
    pub fn func3() {
        println!("我是公有函數 func3");
        // 私有的條目對外是不可見的
        // 但是對內可見
        func1();
    }
}
fn main() {
    // 通過模塊 a 只能找到 func2 和 func3
    // func1 是私有的,外界無法獲取
    a::func2();
    /*
    我是公有函數 func2
    */
    a::func3();
    /*
    我是公有函數 func3
    我是私有函數 func1
    */
}

通過模塊名可以調用模塊內部的條目,訪問方式是通過 ::。但是要注意條目的可見性,默認都是私有的,如果想通過模塊名訪問,那麼一定要在條目的前面加上 pub 關鍵字。

那麼可能有人好奇了,爲啥 mod a 的前面沒有 pub 呢?原因是模塊 a 位於最外層,和調用方處於同一級,因此不需要 pub。

另外在 Rust 裏面還有一個 crate 的概念,crate 是 Rust 最小的編譯單元,在一個範圍內將多個文件裏面的功能組合在一起,最終通過編譯生成一個二進制文件或庫文件。所以 crate 是項目中多個文件的組合,整體形成一棵樹,並且其中一個是入口文件、也就是它的根,對於當前來說顯然是 main.rs。

因此我們還可以這麼調用:

fn main() {
    crate::a::func2();
    /*
    我是公有函數 func2
    */
    crate::a::func3();
    /*
    我是公有函數 func3
    我是私有函數 func1
    */
}

通過 crate 即可從指定文件裏面查找指定條目,並且這種方式相當於使用絕對路徑進行查找,因此它永遠都是成立的。另外可能有人發現了,這裏 crate 後面沒有指定文件啊,因爲 main.rs 是入口文件,如果不指定文件的話,那麼查找的默認是 main.rs 裏的條目。

然後模塊也是可以嵌套的,我們在模塊 a 裏面還可以繼續定義模塊 b。

mod a {
    pub mod b {
        pub fn func() {
            println!("我是模塊 a/b 下的 func");
        }
    }
}
fn main() {
    // 想要調用裏面的 func 函數
    // 那麼模塊 b 和 func 函數都必須是公有的
    a::b::func();
    /*
    我是模塊 a/b 下的 func
    */
}

mod 內部的條目之間,也可以相互調用,舉個例子:

mod a {
    pub fn mod_a_f1() {
        b::mod_b_f2()
    }
    // 注意:mod b 不是公有的
    mod b {
        pub fn mod_b_f2() {
            println!("我是模塊 a/b 下的函數 f2")
        }
    }
}

fn main() {
    a::mod_a_f1();
    /*
    我是模塊 a/b 下的函數 f2
    */

    // a::b::mod_b_f2(); // 不合法
}

我們在函數 mod_a_f1 的內部調用模塊 b 的一個函數,顯然整個過程不需要解釋,但模塊 b 不是公有的爲啥也能訪問呢?很簡單,因爲函數 mod_a_f1 和模塊 b 是在同一級,所以可以直接拿到模塊 b,因此模塊 b 可以不是公有的,但它內部的函數必須公有。

而 main 函數和模塊 b 顯然就不是同一級了,它們之間有一個屏障,也就是模塊 a。但模塊 b 在模塊 a 裏面不是公有的,因此在 main 函數里面無法通過 a::b 的方式獲取。

然後上面是在父模塊內部調用子模塊的條目,因此條目必須公有;但如果是子模塊調用父模塊的條目,那麼條目是否公有就都無所謂了。

mod a {
    // 函數是私有的
    fn mod_a_f1() {
        println!("我是模塊 a 下的函數 f1")
    }

    pub mod b {
        pub fn mod_b_f2() {
            println!("我是模塊 a/b 下的函數 f2");
            // 想在這裏調用 mod_a_f1 要怎麼做呢?
            // 首先要找到模塊 a,但它和 a 不在同一級
            // 因此無法使用 a::mod_a_f1() 的方式
            crate::a::mod_a_f1();
            // 需要使用絕對路徑,從 crate 開始定位
        }
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模塊 a/b/c 下的函數 f3");
                // 它是一個嵌套在模塊 b 裏面的模塊
                // 如果也要調用 mod_a_f1,顯然方法和上面相同
                // 但除此之外還有一種方式,就是使用 super
                super::super::mod_a_f1();
                // super 表示獲取當前所在模塊的上一級模塊
                // 一個 super 獲取到的顯然是模塊 b
                // 兩個 super 獲取到的就是模塊 a 了
            }
        }
    }
}

fn main() {
    // 首先 main 函數里的 crate::a::mod_a_f1() 是不合法的;
    // 因爲 mod_a_f1 在模塊 a 的內部是不可見的

    // 但 mod_b_f2 裏面也是 crate::a::mod_a_f1() 啊,爲啥它就合法呢
    // 因爲子 mod 中的條目如果私有,對於父 mod 是不可見的
    // 但父 mod 中的條目無論公有還是私有,子 mod 都是可見的
    // 所以模塊 a 的 mod_a_f1,對於在 crate 裏面的 main 函數來說不可見
    // 但對於在模塊 b 裏面的 mod_b_f2 來說是可見的
    // 因此調用方式是一樣的,唯一的區別就是調用位置所導致的可見性問題
    a::b::mod_b_f2();
    /*
    我是模塊 a/b 下的函數 f2
    我是模塊 a 下的函數 f1
    */

    a::b::c::mod_c_f3();
    /*
    我是模塊 a/b/c 下的函數 f3
    我是模塊 a 下的函數 f1
    */
}

還是很好理解的,需要注意的是裏面的 super。從父模塊找子模塊的話,直接一級一級往下找即可,但子模塊找父模塊則需要從 crate 開始,有時會比較麻煩。爲此 Rust 提供了 super,用於定位上一級模塊。

然後我們再來看一個好玩的:

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模塊 a/b/c 下的函數 f3");
                // super 表示上一級模塊
                // super::super 顯然就是上上一級,也就是模塊 a
                // 那麼 super::super::super 表示啥呢?顯然是 crate
                // 而通過 crate 即可找到 main 函數和模塊 a
                super::super::super::main();
            }
        }
    }
}

fn main() {
    println!("main 函數被調用");
    a::b::c::mod_c_f3();
}

你覺得這段代碼執行的時候會發生什麼現象呢?我們試一下。

我們看到因爲無限遞歸導致棧溢出了,相信你應該明白模塊之間的關係了,多個 rs 文件整體組成一個 crate,基於 crate 可以獲取每一個文件的條目。此方法相當於使用絕對路徑定位,因此無論在什麼情況下它都是可靠的。但如果模塊嵌套的比較深,那麼通過 crate 一級一級查找就有點麻煩了,比如我們要獲取相鄰模塊(比如上一級)內部的條目,這種情況下 Rust 推薦使用  super。

另外上面使用三個 super 找到了 crate,如果是四個 super 呢?顯然會報錯,因爲 crate 已經是最頂層了。

結構體的可見性

結構體可見分爲兩部分,一個是結構體本身是否可見,另一個是字段是否可見。

結構體的 age 字段不是公有的,所以實例化的時候會報錯。那如果實例化的時候不指定 age 會怎麼樣,答案是也會報錯,因爲 Rust 要求每一個字段都必須指定。所以如果你不希望某個字段被外界訪問,那麼就可以將其定義爲私有,然後通過專門的方法進行實例化。

mod a {
    #[derive(Debug)]
    pub struct Girl {
        pub name: String,
        age: u8,
    }
    // 我們是爲結構體實現方法,重點是方法
    // 所以 impl 的前面不需要 pub,也不能加 pub
    impl Girl {
        pub fn new(name: String, age: u8) -> Girl {
            Girl { name: name, age: age }
        }
    }
}
fn main() {
    let g = a::Girl::new(String::from("古明地覺"), 17);
    println!("{:?}", g);
    /*
    Girl { name: "古明地覺", age: 17 }
    */
}

new 方法也必須是公有的,否則在 main 裏面無法調用。然後在創建結構體實例之後,也只能訪問公有字段,不能訪問私有字段。

枚舉也是同樣的道理,但枚舉的成員不存在公有私有,只要枚舉是公有的,那麼內部的成員都可以訪問。

mod a {
    #[derive(Debug)]
    pub enum Color {
        RGB(u8, u8, u8),
        HSV(u8, u8, u8)
    }
}
fn main() {
    let c = a::Color::RGB(133, 125, 89);
    println!("{:?}", c);
    /*
    RGB(133, 125, 89)
    */
}

還是比較簡單的,然後我們這裏使用條目的時候,都必須通過 模塊:: 的方式,難免有些麻煩。於是我們可以使用 use 關鍵字將感興趣的條目,引入到當前作用域。

use 聲明

use 關鍵字可以將指定的條目引入當前作用域,用於簡化模塊查找過程。

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模塊 a/b/c 下的函數 f3");
            }
        }
    }
}

fn main() {
    // 引入指定模塊,這裏通過絕對路徑
    use crate::a::b;
    // 然後便可以通過 b 來進行查找
    b::c::mod_c_f3();  //我是模塊 a/b/c 下的函數 f3

    // 引入模塊,通過相對路徑
    use a::b::c;
    c::mod_c_f3();  //我是模塊 a/b/c 下的函數 f3

    // 還可以導入到某一個具體的函數
    use a::b::c::mod_c_f3;
    mod_c_f3();  //我是模塊 a/b/c 下的函數 f3
}

所以當模塊層級比較多的時候,我們還可以使用 use 將指定的模塊單獨導入進來,這樣在使用的時候就沒必要從最外層開始找了。當然啦,我們在導入的時候還可以起別名。

fn main() {
    use a::b::c as cc;
    cc::mod_c_f3();

    use a::b::c::mod_c_f3 as mod_c_f33333;
    mod_c_f33333();  
}

結果是一樣的,總之在導入模塊的時候,可以通過 as 起別名。

super 和 self

我們之前在查找條目的時候,使用了 super 關鍵字,它表示當前模塊的上一級模塊。然後除了 super,還有 self,它表示當前模塊。

mod a {
    pub mod b {
        mod c {
            pub fn func1() {
                println!("我是 func1")
            }
        }
        // func2 所在的模塊是 b
        pub fn func2() {
            // 兩者是等價的
            // 但是使用 self,語義會更加的明確
            c::func1();
            self::c::func1();
        }
    }
}
fn main() {
    a::b::func2();
    /*
    我是 func1
    我是 func1
    */
}

從結果上沒有區別,但通過 self 可以消除路徑硬編碼。

最後,我們上面都是手動定義一個模塊,Rust 還可以導入文件,以及導入包。關於這方面的內容,可以點擊閱讀之前寫過的一篇文章

錯誤處理

首先說一下錯誤 (Error) 和異常(Exception),有很多人分不清這兩者的區別,我們來解釋一下。在 Python 裏面很少會對錯誤和異常進行區分,甚至將它們視做同一種概念。但在 Go 和 Rust 裏面,錯誤和異常是完全不同的,異常要比錯誤嚴重得多。

當出現錯誤時,開發者是有能力解決的,比如文件不存在。這時候程序並不會有異常產生,而是正常執行,只是作爲返回值的 error 不爲空,開發者要基於 error 進行下一步處理。但如果出現了異常,那麼一定是代碼寫錯了,開發者無法處理了。比如索引越界,程序會直接 panic 掉,所以在 Rust 裏面異常又叫做不可恢復的錯誤。

不可恢復的錯誤

如果在 Rust 裏面出現了異常,也就是不可恢復的錯誤,那麼就表示開發者希望程序立刻中止掉,不要再執行下去了。而不可恢復的錯誤,除了程序在運行過程中因爲某些原因自然產生之外,也可以手動引發,主要通過以下幾個宏。

fn main() {
    // 調用 panic! 宏引發不可恢復錯誤
    // 該宏支持字符串格式化
    panic!("發生了不可恢復的錯誤");

    // 調用 assert! 宏,當條件不滿足時
    // 引發錯誤
    assert!(1 == 2);
    // 還有兩個作用類似的宏
    // 等價於 assert!(1 == 2)
    assert_eq!(1, 2);
    // 等價於 assert!(1 != 2)
    assert_ne!(1, 2);

    // 當某個功能尚未實現時,一般使用該宏
    unimplemented!("還沒開發完畢, by {}""古明地覺");

    // 當程序執行到了一個不可能出現的位置時
    // 使用該宏
    unreachable!("程序不可能執行到這裏");
}

以上就是 Rust 裏面的幾個用於創建不可恢復的錯誤的幾個宏。

然後再來看看如何處理可恢復的錯誤,這是我們的重點。

可恢復的錯誤

可恢復的錯誤一般稱之爲錯誤,在 Go 裏面錯誤是通過多返回值實現的,如果程序可能出現錯誤,那麼會多返回一個 error,然後根據 error 是否爲空來判斷究竟有沒有產生錯誤。所以開發者必須先對 error 進行處理,然後纔可以執行下一步,不應該對 error 進行假設。

而 Rust 的錯誤機制和 Go 類似,只不過是通過枚舉實現的,該枚舉叫 Result,我們看一下它的定義。

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

如果將定義簡化一下,那麼就是這個樣子。可以看到它就是一個簡單的枚舉,並且帶有兩個泛型。我們之前也介紹過一個枚舉叫 Option,用來處理空值的,內部有兩個成員,分別是 Some 和 None。

然後枚舉 Result 和 Option 一樣,它和內部的成員都是可以直接拿來用的,我們實際舉個例子演示一下吧。

// 計算兩個 i32 的商
fn divide(a: i32, b: i32) -> Result<i32, &'static str> {
    let ret: Result<i32, &'static str>;
    // 如果 b != 0,返回 Ok(a / b)
    if b != 0 {
        ret = Ok(a / b);
    } else {
        // 否則返回除零錯誤
        ret = Err("ZeroDivisionError: division by zero")
    }
    return ret;
}

fn main() {
    let a = divide(100, 20);
    println!("a = {:?}", a);

    let b = divide(100, 0);
    println!("b = {:?}", b);
    /*
    a = Ok(5)
    b = Err("ZeroDivisionError: division by zero")
    */
}

因爲 Rust 返回的是枚舉,比如上面代碼中的 a 是一個 Ok(i32),即便沒有發生錯誤,這個 a 也不能直接用,必須使用 match 表達式處理一下。

fn main() {
    // 將返回值和 5 相加,由於 a 是 Ok(i32)
    // 顯然它不能直接和 i32 相加
    let a = divide(100, 20);
    match a {
        Ok(i) => println!("a + 5 = {}", i + 5),
        Err(error) => println!("出錯啦: {}", error),
    }

    let b = divide(100, 0);
    match b {
        Ok(i) => println!("b + 5 = {}", i + 5),
        Err(error) => println!("出錯啦: {}", error),
    }
    /*
    a + 5 = 10
    出錯啦: ZeroDivisionError: division by zero
    */
}

雖然這種編碼方式會讓人感到有點麻煩,但它杜絕了出現運行時錯誤的可能。相比運行時報錯,我們寧可在編譯階段多費些功夫。

unwrap

Rust 的錯誤通過 Result 枚舉實現,裏面有 Ok 和 Err 兩個成員。如果沒有發生錯誤,那麼將值用 Ok 包裝一下返回,如果發生錯誤了,那麼將錯誤用 Err 包裝返回。

這樣在拿到返回值的時候,使用 match 表達式進行處理。但說實話這樣其實有點麻煩,如果返回的是 Ok(...),那麼我們能不能直接把 Ok 裏面的值拿到呢?答案是可以的,就是使用 unwrap。

fn test(age: u8) -> Result<String, String> {
    if age >= 18 {
        Ok(String::from("歡迎來到極樂淨土"))
    } else {
        Err(String::from("未成年"))
    }
}

fn main() {
    let res = test(18);
    println!("{:?}", res);
    /*
    Ok("歡迎來到極樂淨土")
    */
    let res = test(17);
    println!("{:?}", res);
    /*
    Err("未成年")
    */

    // 可以使用 match 拿到 Ok 裏面的值
    // 但還有沒有更簡單的辦法呢
    let res = test(20).unwrap();
    println!("{}", res);
    /*
    歡迎來到極樂淨土
    */
}

Result 類型的值有一個 unwrap 方法,如果返回的是 Ok,那麼會直接將值取出來。如果返回的 Err,那麼會 panic 掉。

fn main() {
    let res: Result<&str, &str> = Ok("嘿嘿");
    println!("{}", res.unwrap());  // 嘿嘿

    let res: Result<&str, &str> = Err("哈哈");
    // 此處會直接 panic 掉
    println!("{}", res.unwrap());
}

Result 是可恢復的錯誤,通過 match 表達式可以對 Err 進行處理。但這樣有些麻煩,於是可以通過 unwrap 直接將值拿出來,前提返回的是 Ok(...)。如果返回的是 Err(...),那麼 unwrap 就會 panic 掉。

再比如字符串轉整數:

fn main() {
    // 調用字符串的 parse 方法即可轉化
    // 但轉成什麼類型呢?要通過 ::<T> 的方式指定
    let n = "23".parse::<i32>();
    // 返回的是 Result<i32, ParseIntError>
    // Ok 裏面值的類型是什麼,取決於我們要轉成什麼類型
    println!("{:?}", n);  // Ok(23)
    // 調用 unwrap,可以直接拿到 Ok 裏面的值
    println!("{}", n.unwrap()); // 23

    // 但如果返回的不是 Ok(...) 呢
    let n = "你好".parse::<i32>();
    // 轉化失敗,但這是可恢復的錯誤,程序不會崩潰掉
    // 我們可以根據實際情況,進行合適的處理
    println!("{:?}", n);
    /*
    Err(ParseIntError { kind: InvalidDigit })
    */
    // 但如果調用 unwrap,那麼當返回的是 Err 時
    // 程序就會直接 panic 掉
    // println!("{}", n.unwrap()) // 此處會 panic 
}

因此當你能確保返回的是 Ok(...),那麼直接 unwrap 即可,但也要承擔因判斷失誤而引發 panic 的風險。

另外不光是 Result,Option 枚舉也是可以這麼做的。如果是 Some,調用 unwrap 會返回 Some 裏面的值;如果是 None,調用 unwrap 則 panic。

fn main() {
    let n = Some(666).unwrap();
    println!("{}", n); // 666

    let n: Option<i32> = None;
    // 會 panic
    // println!("{}", n.unwrap());
}

所以通過 unwrap,我們能夠簡化代碼的邏輯。

and_then 方法

Result 類型還提供了 and_then 方法,來看一下它的用法:

fn main() {
    let n1: Result<i32, &'static str> = Ok(123);
    // 如果是 Ok(...),那麼將裏面的值乘以 2
    // 如果是 Err(...),那麼保持不變
    // 你也許會這麼做
    let n2 = match n1 {
        Ok(val) => Ok(val * 2),
        Err(success) => Err(success),
    };
    println!("{:?}", n2);  // Ok(246)
    // 上面是一種做法,但還可以通過 and_then 進行簡化
    // 如果 n1 是 Ok(...),那麼會將 Ok 裏面的值取出來
    // 放到匿名函數當中調用
    let n2 = n1.and_then(|x: i32| { Ok(x * 2) });
    println!("{:?}", n2);  // Ok(246)

    // 如果 n1 是 Err(...)
    let n1 = Err("出錯啦");
    // 那麼不會執行 and_then,直接返回 Err(...)
    let n2 = n1.and_then(
        |x: i32| { println!("此處不會打印"); Ok(x * 2) }
    );
    println!("{:?}", n2);  // Err("出錯啦")
}

之前在面對 Result 的時候,使用的是 match 表達式,但有時候不太方便,於是便有了 unwrap。Ok(...) 在調用 unwrap 的時候可以直接把值拿出來,但如果是 Err(...),則直接 panic。

於是現在又有了 and_then,它接收一個函數作爲參數。and_then 相比 unwrap 的好處就在於,如果是 Err(...),那麼程序不至於崩潰掉,而是直接把錯誤原封不動地返回。如果是 Ok(...) 調用 and_then,那麼同樣會將 Ok 裏面的值拿出來,然後傳到 and_then 接收的函數里面去進行調用,最後將它的返回值返回。

fn main() {
    let n1: Result<i32, &'static str> = Ok(6);
    // (6 * 2 + 1)^2
    let n2 = n1.and_then(|x| Ok(x * 2))
    .and_then(|x| Ok(x + 1))
    .and_then(|x| Ok(x * x));
    println!("{:?}", n2); // Ok(169)
}

我們再看一個更復雜的例子:

fn main() {
    let n1: Result<i32, &'static str> = Ok(6);
    let n2 = n1
        // 會將 Ok(6) 裏面的 6 取出來
        // 傳到 and_then 裏面的函數進行執行
        .and_then(|x: i32| {
            println!("x * 2");
            Ok(x * 2)
        })
        // 同樣的道理,但它返回的是 Err(...)
        .and_then(|x: i32| {
            println!("x + 1");
            Err("在 x + 1 這一步出錯了")
        })
        // 因爲上一步返回了 Err(...)
        // 所以此處的 and_then 不會執行
        .and_then(|x: i32| {
            println!("x * x");
            Ok(x * x)
        });
    println!("{:?}", n2);
    /*
    x * 2
    x + 1
    Err("在 x + 1 這一步出錯了")
    */
}

相信你對 and_then 的用法已經充分了解了,如果你不關心程序是否 panic,或者確保它一定不會 panic,那麼最簡單的做法就是使用 unwrap。但如果你無法保證,並且還希望出現 Err 的時候程序正常執行,那麼使用 and_then,將處理邏輯寫在一個函數里,然後作爲參數傳給 and_then。

另外不光 Result 可以使用 and_then,Option 也是可以的。

fn main() {
    let n1: Option<i32> = Some(6);
    // 要注意 and_then 裏面函數的返回值類型
    // 調用它的 n1 是 Option 類型,所以函數也要返回 Option類型
    let n2 = n1.and_then(|x| Some(x * 2));
    println!("{:?}", n2); // Some(12)

    // 調用 and_then 的如果是 Some(...)
    // 那麼和 Ok 一樣,會將 Some 裏面的值取出來
    // 傳到 and_then 接收的函數里面,進行調用
    // 但如果是 None 調用的 and_then,則直接返回 None
    let n2 = n1
        .and_then(|x| {
            println!("x * 2");
            Some(x * 2)
        })
        // 這裏要指定返回值的類型,Option<T>
        // 因爲出現了 None 的話會直接返回
        // 而只有一個 None,Rust 無法推斷 T 的類型
        .and_then(|x| -> Option<i32> {
            println!("x + 1");
            None
        })
        .and_then(|x| {
            println!("x * x");
            Some(x * x)
        });
    println!("{:?}", n2);
    /*
    x * 2
    x + 1
    None
    */
}

所以在 and_then 方法的使用上,Result 和 Option 是類似的。如果是 Ok 或者 Some,那麼將值取出來傳到 and_then 接收的函數里面去;如果是 Err 或 None,那麼直接返回,不會執行 and_then。

再舉個例子,我們定義一個函數,接收兩個字符串,轉成 i32,計算它們的和。

use std::num::ParseIntError as E;
fn add(a: &str, b: &str) -> Result<i32, E> {
    // 解析成功,返回 Ok(a * b)
    // 解析失敗,直接返回 Err(...),但注意這裏的錯誤
    // 由於解析失敗返回的是 ParseIntError
    // 因此 add 函數的錯誤類型也要是 ParseIntError
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}

fn main() {
    println!("{:?}", add("12""33"));
    println!("{:?}", add("a""b"));
    /*
    Ok(45)
    Err(ParseIntError { kind: InvalidDigit })
    */
}

是不是很方便呢?有了 and_then,我們就可以不用 match 了。當然 match 雖然複雜了一些,但它的好處就是我們可以自定義錯誤處理邏輯。當然了,match 和 and_then 也可以結合起來使用。

然後我們上面爲了避免函數定義過長,使用了取別名的做法,但取別名還有另一種方式。

use std::num::ParseIntError as E;
type ResultWithParseInt<T> = Result<T, E>;
fn add(a: &str, b: &str) -> ResultWithParseInt<i32> {
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}

這種做法也是可以的。

最後除了 and_then,還有很多其它方法,比如 map, map_or, map_err 等等,可以瞭解一下。

問號表達式

先來回顧一下我們處理錯誤的幾種方式: 

type T = Result<i32, &'static str>;
// 使用 match
fn use_match(n: T) -> T {
    // 針對不同分支可以做出不同的處理
    // 包括 error 也可以自定製
    match n {
        Ok(i) => Ok(i * 2),
        Err(error) => Err(error)
    }
}

// 使用 unwrap
fn use_unwrap(n: T) -> T {
    // 如果 n 是 Err,那麼此處直接 panic
    let i = n.unwrap();
    Ok(i * 2)
}

// 使用 and_then
fn use_and_then(n: T) -> T {
    // 如果 n 是 Err,那麼錯誤原封不動返回
    n.and_then(|x| Ok(x * 2))
}

這些方式都有不完美的地方,match 和 and_then 不夠簡潔,至於 unwrap 雖然簡單,但它會 panic。特別是當錯誤需要在上下文當中傳遞的時候,這三種方式都不夠好,那麼有沒有更簡單的做法呢,顯然是有的。

首先 Rust 爲了避免控制流混亂,並沒有引入 try cache 語句。但 try cache 也有它的好處,就是可以完整地記錄堆棧信息,從錯誤的根因到出錯的地方,都能完整地記錄下來,舉個 Python 的例子:

程序報錯了,根因是調用了函數 f,而出錯的地方是在第 10 行,我們手動 raise 了一個異常。可以看到程序將整個錯誤的鏈路全部記錄下來了,只要從根因開始一層層往下定位,就能找到錯誤原因。

而對於 Go 和 Rust 來說就不方便了,特別是 Go,如果每返回一個 error,就打印一次,那麼會將 error 打的亂七八糟的。所以我們更傾向於錯誤能夠在上下文當中傳遞,對於 Rust 而言,雖然 match 和 and_then 可以實現,但不夠簡潔。我們更推薦使用問號表達式來實現這一點。

fn external_some_func() -> Result<u32, &'static str> {
    // 外部的某個函數
    Ok(666)
}

fn call1() -> Result<f64, &'static str> {
    // 我們要調用 external_some_func
    match external_some_func() {
        // 類型轉化在 Rust 裏面通過 as 關鍵字
        Ok(i) => Ok((i + 1) as f64),
        Err(error) => Err(error)
    }
}

// 但是上面這種調用方式有點繁瑣
// 我們還可以使用問號表達式
fn call2() -> Result<f64, &'static str> {
    // 注:使用問號表達式有一個前提
    // 調用方和被調用方的返回值都要是 Result 枚舉類型
    // 並且它們的錯誤類型要相同,比如這裏都是 &'static str
    let ret = external_some_func()?;
    Ok((ret + 1) as f64)
}

fn main() {
    println!("{:?}", call1());  // Ok(667.0)
    println!("{:?}", call2());  // Ok(667.0)
}

裏面的 call1 和 call2 是等價的,如果在 call2 裏面函數調用出錯了,那麼會自動將錯誤返回。並且注意 call2 裏面的 ret,它是 u32,不是 Ok(u32)。因爲函數調用出錯會直接返回,不出錯則會將 Ok 裏面的 u32 取出來賦值給 ret。

所以問號表達式等價於不會 panic 的 unwrap,這也正是我們需要的,否則就要使用 match 表達式,或者調用 and_then 並往裏面傳入一個函數。對於只關心 Ok,而 Err 直接返回的場景來說,使用問號表達式是最合適的。

問號表達式完全可以使用 match 和 and_then 實現,但問號表達式無疑是最方便的。

所以之前的一個例子:定義一個函數,接收兩個字符串,轉成 i32,計算它們的和,就可以這麼改。

use std::num::ParseIntError as E;
// 使用 and_then
fn add1(a: &str, b: &str) -> Result<i32, E> {
    a.parse::<i32>().and_then(|a| {
        b.parse::<i32>().and_then(|b| {
            Ok(a + b)
        })
    })
}
// 使用問號表達式
fn add2(a: &str, b: &str) -> Result<i32, E> {
    Ok(a.parse::<i32>()? + b.parse::<i32>()?)
}

fn main() {
    println!("{:?}", add1("11""22"));
    println!("{:?}", add2("11""22"));
    /*
    Ok(33)
    Ok(33)
    */
    println!("{:?}", add1("a""b"));
    println!("{:?}", add2("a""b"));
    /*
    Err(ParseIntError { kind: InvalidDigit })
    Err(ParseIntError { kind: InvalidDigit })
    */
}

顯然問號表達式是最方便的。

另外在 ? 出現以前,相同的功能是使用 try! 宏完成的,但是現在推薦使用 ? 表達式,不過在老代碼中仍然會看到 try!。比如 try!(xxx()) 等價於 xxx()?。

同時處理多種錯誤

再來考慮一種更復雜的情況,我們在調用函數的時候可能會調用多個函數,而這多個函數的錯誤類型不一樣該怎麼辦呢?

#[derive(Debug)]
struct FileNotFoundError {
    err: String,
    filename: String,
}

#[derive(Debug)]
struct IndexError {
    err: &'static str,
    index: u32,
}

fn external_some_func1() -> Result<u32, FileNotFoundError> {
    Err(FileNotFoundError {
        err: String::from("文件不存在"),
        filename: String::from("main.py"),
    })
}

fn external_some_func2() -> Result<i32, IndexError> {
    Err(IndexError {
        err: "索引越界了",
        index: 9,
    })
}

很多時候,錯誤並不是一個簡單的字符串,因爲那樣能攜帶的信息太少。基本上都是一個結構體,文字格式的錯誤信息只是裏面的字段之一,而其它字段則負責描述更加詳細的上下文信息。

我們上面有兩個函數,是一會兒我們要調用的,但問題是它們返回的錯誤類型不同,也就是 Result<T, E> 裏面的 E 不同。而如果是這種情況的話,問號表達式就會失效,那麼我們應該怎麼做呢?

// 其它代碼不變
#[derive(Debug)]
enum MyError {
    Error1(FileNotFoundError),
    Error2(IndexError)
}

// 爲 MyError 實現 From trait
// 分別是 From<FileNotFoundError> 和 From<IndexError>
impl From<FileNotFoundError> for MyError {
    fn from(error: FileNotFoundError) -> MyError {
        MyError::Error1(error)
    }
}

impl From<IndexError> for MyError {
    fn from(error: IndexError) -> MyError {
        MyError::Error2(error)
    }
}

fn call1() -> Result<i32, MyError>{
    // 調用的兩個函數、和當前函數返回的錯誤類型都不相同
    // 但是當前函數是合法的,因爲 MyError 實現了 From trait
    // 當錯誤類型是 FileNotFoundError 或 IndexError 時
    // 它們會調用 MyError 實現的 from 方法
    // 然後將錯誤統一轉換爲 MyError 類型
    let x = external_some_func1()?;
    let y = external_some_func2()?;
    Ok(x as i32 + y)
}

fn call2() -> Result<i32, MyError>{
    let y = external_some_func2()?;
    let x = external_some_func1()?;
    Ok(x as i32 + y)
}

fn main() {
    println!("{:?}", call1());
    /*
    Err(Error1(FileNotFoundError { err: "文件不存在", filename: "main.py" }))
    */
    println!("{:?}", call2());
    /*
    Err(Error2(IndexError { err: "索引越界了", index: 9 }))
    */
}

如果調用的多個函數返回的錯誤類型相同,那麼只需要保證調用方也返回相同的錯誤類型,即可使用問號表達式。但如果調用的多個函數返回的錯誤類型不同,那麼這個時候調用方就必須使用一個新的錯誤類型,其數據結構通常爲枚舉。

而枚舉裏的成員要包含所有可能發生的錯誤類型,比如這裏的 FileNotFoundError 和 IndexError。然後爲枚舉實現 From trait,該 trait 帶了一個泛型,並且內部定義了一個 from 方法。

我們在實現之後,當出現 FileNotFoundError 和 IndexError 的時候,就會調用 from 方法,轉成調用方的 MyError 類型,然後返回。

因此這就是 Rust 處理錯誤的方式,可能有一些難理解,需要私下多琢磨琢磨。最後再補充一點,我們知道 main 函數應該返回一個空元組,但除了空元組之外,它也可以返回一個 Result。

fn main() -> Result<(), MyError> {
    // 如果 call1() 的後面沒有加問號
    // 那麼在調用沒有出錯的時候,返回的就是 Ok(...)
    // 調用出錯的時候,返回的就是 Err(...)
    // 但不管哪一種,都是 Result<T, E> 類型
    println!("{:?}", call1());

    // 如果加了 ? 那麼就不一樣了
    // 在調用沒出錯的時候,會直接將 Ok(...) 裏面的值取出來
    // 調用出錯的時候,當前函數會中止運行,
    // 並將被調用方(這裏是 call2)的錯誤作爲調用方(這裏是 main)的返回值返回
    // 此時通過問號表達式,就實現了錯誤在上下文當中傳遞
    // 所以這也要求被調用方返回的錯誤類型要和調用方相同
    println!("{:?}", call2()?);

    // 爲了使函數簽名合法,這裏要返回一個值,直接返回 Ok(()) 即可
    // 但上面的 call2()? 是會報錯的,所以它下面的代碼都不會執行
    Ok(())
}

我們執行一下看看輸出:

由於 main 函數已經是最頂層的調用方了,所以出錯的時候,直接將錯誤拋出來了。

小結

以上就是 Rust 的模塊和錯誤處理,相比其它語言來說,確實難理解了一些。到目前爲止,我們簡單回顧了之前介紹的內容,後續開始學習新的內容。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1Fx0ZCHffq9e7vbYUifNjQ