溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Rust是怎么處理錯(cuò)誤的

發(fā)布時(shí)間:2022-12-01 10:16:05 來(lái)源:億速云 閱讀:76 作者:iii 欄目:開(kāi)發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“Rust是怎么處理錯(cuò)誤的”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“Rust是怎么處理錯(cuò)誤的”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來(lái)學(xué)習(xí)新知識(shí)吧。

異常的演進(jìn)

程序在運(yùn)行的過(guò)程中,總是會(huì)不可避免地產(chǎn)生錯(cuò)誤,而如何優(yōu)雅地解決錯(cuò)誤,也是語(yǔ)言的設(shè)計(jì)哲學(xué)之一。那么現(xiàn)有的主流語(yǔ)言是怎么處理錯(cuò)誤的呢?比如調(diào)用一個(gè)函數(shù),如果函數(shù)執(zhí)行的時(shí)候出錯(cuò)了,那么該怎么處理呢。

C 語(yǔ)言

C 是一門(mén)古老的語(yǔ)言,通常會(huì)以指針作為參數(shù),在函數(shù)內(nèi)部進(jìn)行解引用,修改指針指向的值。然后用 1 和 0 代表返回值,如果返回 1,則表示修改成功;返回 0,表示修改失敗。

但這種做法有一個(gè)缺陷,就是修改失敗時(shí),無(wú)法將原因記錄下來(lái)。

C++ 和 Python

引入了 Exception,通過(guò) try catch 可以將異常捕獲,相比 C 進(jìn)步了一些。但它的缺陷是我們不知道被調(diào)用方會(huì)拋出什么異常。

Java

引入了 checked exception,方法的所有者可以聲明自己會(huì)拋出什么異常,然后調(diào)用者對(duì)異常進(jìn)行處理。在 Java 程序啟動(dòng)時(shí),拋出大量異常都是司空見(jiàn)慣的事情,并在相應(yīng)的調(diào)用堆棧中將信息完整地記錄下來(lái)。至此,Java 的異常不再是異常,而是一種很普遍的結(jié)構(gòu),從良性到災(zāi)難性都有所使用,異常的嚴(yán)重性由調(diào)用者來(lái)決定。

而像 Go、Rust 這樣的新興語(yǔ)言,則采用了與之不同的方式。它們沒(méi)有像傳統(tǒng)的高級(jí)語(yǔ)言一樣引入 try cache,因?yàn)樵O(shè)計(jì)者認(rèn)為這會(huì)把控制流搞得非常亂。在 Go 和 Rust 里面,錯(cuò)誤是通過(guò)返回值體現(xiàn)的。

比如打開(kāi)一個(gè)文件,如果文件不存在,像 Python 程序就會(huì)直接報(bào)錯(cuò)。但 Go 不一樣,Go 在打開(kāi)文件的時(shí)候會(huì)同時(shí)返回一個(gè)文件句柄和 error,如果文件成功打開(kāi),那么 error 就是空;如果文件打開(kāi)失敗,那么 error 就是錯(cuò)誤原因。

所以對(duì)于 Go 而言,在可能出錯(cuò)的時(shí)候,程序會(huì)同時(shí)返回 value 和 error。如果你要使用 value,那么必須先對(duì) error 進(jìn)行判斷。

錯(cuò)誤和異常

我們上面提到了錯(cuò)誤(Error)和異常(Exception),有很多人分不清這兩者的區(qū)別,我們來(lái)解釋一下。

在 Python 里面很少會(huì)對(duì)錯(cuò)誤和異常進(jìn)行區(qū)分,甚至將它們視做同一種概念。但在 Go 和 Rust 里面,錯(cuò)誤和異常是完全不同的,異常要比錯(cuò)誤嚴(yán)重得多。

當(dāng)出現(xiàn)錯(cuò)誤時(shí),開(kāi)發(fā)者是有能力解決的,比如文件不存在。這時(shí)候程序并不會(huì)有異常產(chǎn)生,而是正常執(zhí)行,只是作為返回值的 error 不為空,開(kāi)發(fā)者要基于 error 進(jìn)行下一步處理。

但如果出現(xiàn)了異常,那么一定是代碼寫(xiě)錯(cuò)了,開(kāi)發(fā)者無(wú)法處理了。比如索引越界,程序會(huì)直接 panic 掉,所以在 Rust 里面異常又叫做不可恢復(fù)的錯(cuò)誤。

不可恢復(fù)的錯(cuò)誤

如果在 Rust 里面出現(xiàn)了異常,也就是不可恢復(fù)的錯(cuò)誤,那么就表示開(kāi)發(fā)者希望程序立刻中止掉,不要再執(zhí)行下去了。

而不可恢復(fù)的錯(cuò)誤,除了程序在運(yùn)行過(guò)程中因?yàn)槟承┰蜃匀划a(chǎn)生之外,也可以手動(dòng)引發(fā)。

fn main() {
    println!("程序開(kāi)始執(zhí)行");
    // 在 Go 里面引發(fā)異常通過(guò) panic 函數(shù)
    // Rust 則是通過(guò) panic! 宏,還是挺相似的
    panic!("發(fā)生了不可恢復(fù)的錯(cuò)誤");
    println!("程序不會(huì)執(zhí)行到這里");
}

注意 panic! 和 println! 的參數(shù)一致的,都支持字符串格式化輸出。下面看一下輸出結(jié)果:

Rust是怎么處理錯(cuò)誤的

如果將環(huán)境變量 RUST_BACKTRACE 設(shè)置為 1,還可以顯示調(diào)用棧。

然后除了 panic! 之外,assert 系列的宏也可以生成不可恢復(fù)的錯(cuò)誤。

fn main() {
    // 如果 assert! 里面的布爾值為真,無(wú)事發(fā)生
    // 如果為假,那么程序會(huì) panic 掉
    assert!(1 == 2);

    // assert!(1 == 2) 還可以寫(xiě)成
    assert_eq!(1, 2);

    // 除了 assert_eq! 外,還有 assert_ne!
    assert_ne!(1, 2);

    // 不過(guò)最常用的還是 assert!
}

還有一個(gè)宏叫 unimplemented!,當(dāng)我們的代碼還沒(méi)有開(kāi)發(fā)完畢時(shí),為了在別人調(diào)用的時(shí)候能夠提示調(diào)用者,便可以使用這個(gè)宏。

fn get_data() {
    unimplemented!("還沒(méi)開(kāi)發(fā)完畢,by {}", "古明地覺(jué)");
}

fn main() {
    get_data()
}

Rust是怎么處理錯(cuò)誤的

它和 Python 里的 raise NotImplementedError 是比較相似的。

最后在 Rust 里面還有一個(gè)常用的宏,用于表示程序不可能執(zhí)行到某個(gè)地方。

fn divide_by_3(n: u32) -> u32 {
    // 找到可以滿足 3 * i 大于 n 的最小整數(shù) i
    for i in 0 .. {
        if 3 * i > n {
            return i;
        }
    }
    // 顯然程序不可能執(zhí)行到這里
    // 因?yàn)?nbsp;for 循環(huán)是無(wú)限進(jìn)行的,最終一定會(huì) return
    // 但 Rust 在編譯時(shí),從語(yǔ)法上是判斷不出來(lái)的
    // 它只知道這個(gè)函數(shù)目前不完整,因?yàn)槿绻?nbsp;for 循環(huán)結(jié)束,
    // 那么返回值就不符合 u32 類型了,盡管我們知道 for 循環(huán)不可能結(jié)束

    // 為此我們可以隨便 return 一個(gè) u32,并寫(xiě)上注釋
    // "此處是為了保證函數(shù)簽名合法,但程序不會(huì)執(zhí)行到這里"
    // 而更專業(yè)的做法是使用一個(gè)宏
    unreachable!("程序不可能執(zhí)行到這里");
}

如果程序真的執(zhí)行到了該宏所在的地方,那么同樣會(huì)觸發(fā)一個(gè)不可恢復(fù)的錯(cuò)誤。

以上就是 Rust 里面的幾個(gè)用于創(chuàng)建不可恢復(fù)的錯(cuò)誤的幾個(gè)宏。

可恢復(fù)的錯(cuò)誤

說(shuō)完了不可恢復(fù)的錯(cuò)誤,再來(lái)看看可恢復(fù)的錯(cuò)誤,一般稱之為錯(cuò)誤。在 Go 里面錯(cuò)誤是通過(guò)多返回值實(shí)現(xiàn)的,如果程序可能出現(xiàn)錯(cuò)誤,那么會(huì)多返回一個(gè) error,然后根據(jù) error 是否為空來(lái)判斷究竟有沒(méi)有產(chǎn)生錯(cuò)誤。所以開(kāi)發(fā)者必須先對(duì) error 進(jìn)行處理,然后才可以執(zhí)行下一步,不應(yīng)該對(duì) error 進(jìn)行假設(shè)。

而 Rust 的錯(cuò)誤機(jī)制和 Go 類似,只不過(guò)是通過(guò)枚舉實(shí)現(xiàn)的,該枚舉叫 Result,我們看一下它的定義。

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

如果將定義簡(jiǎn)化一下,那么就是這個(gè)樣子??梢钥吹剿褪且粋€(gè)簡(jiǎn)單的枚舉,并且?guī)в袃蓚€(gè)泛型。我們之前也介紹過(guò)一個(gè)枚舉叫 Option,用來(lái)處理空值的,內(nèi)部有兩個(gè)成員,分別是 Some 和 None。

然后枚舉 Result 和 Option 一樣,它和內(nèi)部的成員都是可以直接拿來(lái)用的,我們實(shí)際舉個(gè)例子演示一下吧。

// 計(jì)算兩個(gè) 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 {
        // 否則返回除零錯(cuò)誤
        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")
    */
}

打印結(jié)果如我們所料,但 Rust 和 Go 一樣,都要求我們提前對(duì) error 進(jìn)行處理,并且 Rust 比 Go 更加嚴(yán)格。對(duì)于 Go 而言,在沒(méi)有發(fā)生錯(cuò)誤的時(shí)候,即使我們不對(duì) error 做處理(不推薦),也是沒(méi)問(wèn)題的。而 Rust 不管會(huì)不會(huì)發(fā)生錯(cuò)誤,都要求對(duì) error 進(jìn)行處理。

因?yàn)?Rust 返回的是枚舉,比如上面代碼中的 a 是一個(gè) Ok(i32),即便沒(méi)有發(fā)生錯(cuò)誤,這個(gè) a 也不能直接用,必須使用 match 表達(dá)式處理一下。

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

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

雖然這種編碼方式會(huì)讓人感到有點(diǎn)麻煩,但它杜絕了出現(xiàn)運(yùn)行時(shí)錯(cuò)誤的可能。相比運(yùn)行時(shí)報(bào)錯(cuò),我們寧可在編譯階段多費(fèi)些功夫。

自定義錯(cuò)誤和問(wèn)號(hào)表達(dá)式

我們說(shuō) Rust 為了避免控制流混亂,并沒(méi)有引入 try cache 語(yǔ)句。但 try cache 也有它的好處,就是可以完整地記錄堆棧信息,從錯(cuò)誤的根因到出錯(cuò)的地方,都能完整地記錄下來(lái),舉個(gè) Python 的例子:

Rust是怎么處理錯(cuò)誤的

程序報(bào)錯(cuò)了,根因是調(diào)用了函數(shù) f,而出錯(cuò)的地方是在第 10 行,我們手動(dòng) raise 了一個(gè)異常??梢钥吹匠绦?qū)⒄麄€(gè)錯(cuò)誤的鏈路全部記錄下來(lái)了,只要從根因開(kāi)始一層層往下定位,就能找到錯(cuò)誤原因。

而對(duì)于 Go 和 Rust 來(lái)說(shuō)就不方便了,特別是 Go,如果每返回一個(gè) error,就打印一次,那么會(huì)將 error 打的亂七八糟的。所以我們更傾向于錯(cuò)誤能夠在上下文當(dāng)中傳遞,對(duì)于 Rust 而言,我們可以通過(guò)問(wèn)號(hào)表達(dá)式來(lái)實(shí)現(xiàn)這一點(diǎn)。

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

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

// 但是上面這種調(diào)用方式有點(diǎn)繁瑣
// 我們還可以使用問(wèn)號(hào)表達(dá)式
fn call2() -> Result<f64, &'static str> {
    // 注:使用問(wèn)號(hào)表達(dá)式有一個(gè)前提
    // 調(diào)用方和被調(diào)用方的返回值都要是 Result 枚舉類型
    // 并且它們的錯(cuò)誤類型要相同,比如這里都是 &'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 是等價(jià)的,如果在 call2 里面函數(shù)調(diào)用出錯(cuò)了,那么會(huì)自動(dòng)將錯(cuò)誤返回。并且注意 call2 里面的 ret,它是 u32,不是 Ok(u32)。因?yàn)楹瘮?shù)調(diào)用出錯(cuò)會(huì)直接返回,不出錯(cuò)則會(huì)將 Ok 里面的 u32 取出來(lái)賦值給 ret。

然后我們說(shuō)如果 external_some_func 函數(shù)執(zhí)行出錯(cuò)了,那么 call2 就直接將錯(cuò)誤返回了,程序不會(huì)再往下執(zhí)行。所以這也側(cè)面要求,call2 和 external_some_func 的返回值類型都是 Result,并且里面的錯(cuò)誤類型也要一樣,否則函數(shù)簽名是不合法的。

fn external_some_func() -> Result<u32, &'static str> {
    // 外部的某個(gè)函數(shù)
    Err("函數(shù)執(zhí)行出錯(cuò)")
}

fn call1() -> Result<f64, &'static str> {
    match external_some_func() {
        Ok(i) => Ok((i + 1) as f64),
        Err(error) => Err(error)
    }
}

fn call2() -> Result<f64, &'static str> {
    let ret = external_some_func()?;
    Ok((ret + 1) as f64)
}

fn main() {
    println!("{:?}", call1());  // Err("函數(shù)執(zhí)行出錯(cuò)")
    println!("{:?}", call2());  // Err("函數(shù)執(zhí)行出錯(cuò)")
}

此時(shí)錯(cuò)誤就自動(dòng)地在上下文當(dāng)中傳遞了,并且還更簡(jiǎn)潔,只需要在函數(shù)調(diào)用后面加一個(gè)問(wèn)號(hào)即可。

再來(lái)考慮一種更復(fù)雜的情況,我們?cè)谡{(diào)用函數(shù)的時(shí)候可能會(huì)調(diào)用多個(gè)函數(shù),而這多個(gè)函數(shù)的錯(cuò)誤類型不一樣該怎么辦呢?

struct FileNotFoundError {
    err: String,
    filename: String,
}

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,
    })
}

很多時(shí)候,錯(cuò)誤并不是一個(gè)簡(jiǎn)單的字符串,因?yàn)槟菢幽軘y帶的信息太少?;旧隙际且粋€(gè)結(jié)構(gòu)體,文字格式的錯(cuò)誤信息只是里面的字段之一,而其它字段則負(fù)責(zé)描述更加詳細(xì)的上下文信息。

我們上面有兩個(gè)函數(shù),是一會(huì)兒我們要調(diào)用的,但問(wèn)題是它們返回的錯(cuò)誤類型不同,也就是 Result<T, E> 里面的 E 不同。而如果是這種情況的話,問(wèn)號(hào)表達(dá)式就會(huì)失效,那么我們應(yīng)該怎么做呢?

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

// 為 MyError 實(shí)現(xiàn) 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>{
    // 調(diào)用的兩個(gè)函數(shù)、和當(dāng)前函數(shù)返回的錯(cuò)誤類型都不相同
    // 但是當(dāng)前函數(shù)是合法的,因?yàn)?nbsp;MyError 實(shí)現(xiàn)了 From trait
    // 當(dāng)錯(cuò)誤類型是 FileNotFoundError 或 IndexError 時(shí)
    // 它們會(huì)調(diào)用 MyError 實(shí)現(xiàn)的 from 方法
    // 然后將錯(cuò)誤統(tǒng)一轉(zhuǎn)換為 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 }))
    */
}

如果調(diào)用的多個(gè)函數(shù)返回的錯(cuò)誤類型相同,那么只需要保證調(diào)用方也返回相同的錯(cuò)誤類型,即可使用問(wèn)號(hào)表達(dá)式。但如果調(diào)用的多個(gè)函數(shù)返回的錯(cuò)誤類型不同,那么這個(gè)時(shí)候調(diào)用方就必須使用一個(gè)新的錯(cuò)誤類型,其數(shù)據(jù)結(jié)構(gòu)通常為枚舉。

而枚舉里的成員要包含所有可能發(fā)生的錯(cuò)誤類型,比如這里的FileNotFoundError和IndexError。然后為枚舉實(shí)現(xiàn) From trait,該 trait 帶了一個(gè)泛型,并且內(nèi)部定義了一個(gè) from 方法。

我們?cè)趯?shí)現(xiàn)之后,當(dāng)出現(xiàn) FileNotFoundError 和 IndexError 的時(shí)候,就會(huì)調(diào)用 from 方法,轉(zhuǎn)成調(diào)用方的 MyError 類型,然后返回。

因此這就是 Rust 處理錯(cuò)誤的方式,可能有一些難理解,需要私下多琢磨琢磨。最后再補(bǔ)充一點(diǎn),我們知道 main 函數(shù)應(yīng)該返回一個(gè)空元組,但除了空元組之外,它也可以返回一個(gè) Result。

fn main() -> Result<(), MyError> {
    // 如果 call1() 的后面沒(méi)有加問(wèn)號(hào)
    // 那么在調(diào)用沒(méi)有出錯(cuò)的時(shí)候,返回的就是 Ok(...)
    // 調(diào)用出錯(cuò)的時(shí)候,返回的就是 Err(...)
    // 但不管哪一種,都是 Result<T, E> 類型
    println!("{:?}", call1());

    // 如果加了 ? 那么就不一樣了
    // 在調(diào)用沒(méi)出錯(cuò)的時(shí)候,會(huì)直接將 Ok(...) 里面的值取出來(lái)
    // 調(diào)用出錯(cuò)的時(shí)候,當(dāng)前函數(shù)會(huì)中止運(yùn)行,
    // 并將被調(diào)用方(這里是 call2)的錯(cuò)誤作為調(diào)用方(這里是 main)的返回值返回
    // 此時(shí)通過(guò)問(wèn)號(hào)表達(dá)式,就實(shí)現(xiàn)了錯(cuò)誤在上下文當(dāng)中傳遞
    // 所以這也要求被調(diào)用方返回的錯(cuò)誤類型要和調(diào)用方相同
    println!("{:?}", call2()?);

    // 為了使函數(shù)簽名合法,這里要返回一個(gè)值,直接返回 Ok(()) 即可
    // 但上面的 call2()? 是會(huì)報(bào)錯(cuò)的,所以它下面的代碼都不會(huì)執(zhí)行
    Ok(())
}

我們執(zhí)行一下看看輸出:

Rust是怎么處理錯(cuò)誤的

由于 main 函數(shù)已經(jīng)是最頂層的調(diào)用方了,所以出錯(cuò)的時(shí)候,直接將錯(cuò)誤拋出來(lái)了。

讀到這里,這篇“Rust是怎么處理錯(cuò)誤的”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識(shí)點(diǎn)還需要大家自己動(dòng)手實(shí)踐使用過(guò)才能領(lǐng)會(huì),如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI