Rust 錯誤處理

来源:https://www.cnblogs.com/timefiles/p/17975021
-Advertisement-
Play Games

目錄用 panic! 處理不可恢復的錯誤對應 panic 時的棧展開或終止使用 panic! 的 backtraceWindows設置 RUST_BACKTRACE 環境變數的兩種方式用 Result 處理可恢復的錯誤匹配不同的錯誤不同於使用 match 和 Result<T, E>失敗時 pani ...


目錄

本文略有刪減,原文請訪問錯誤處理

panic! 巨集代表一個程式無法處理的狀態,並停止執行而不是使用無效或不正確的值繼續處理。
Rust 類型系統的 Result 枚舉代表操作可能會在一種可以恢復的情況下失敗,可以使用 Result 來告訴代碼調用者他需要處理潛在的成功或失敗。

用 panic! 處理不可恢復的錯誤

在實踐中有兩種方法造成 panic:執行會造成代碼 panic 的操作(比如訪問超過數組結尾的內容)或者顯式調用 panic! 巨集。

對應 panic 時的棧展開或終止

當出現 panic 時,程式預設會開始 展開(unwinding),這意味著 Rust 會回溯棧並清理它遇到的每一個函數的數據,不過這個回溯並清理的過程有很多工作。另一種選擇是直接 終止(abort),這會不清理數據就退出程式,程式所使用的記憶體需要由操作系統來清理。

如果需要項目的最終二進位文件越小越好,panic 時通過在 Cargo.toml 的 [profile] 部分增加 panic = 'abort',可以由展開切換為終止,如在 release 模式中 panic 時直接終止:

[profile.release]
panic = 'abort'

在一個簡單的程式中調用 panic!:

fn main() {
    panic!("crash and burn");
}

使用 panic! 的 backtrace

讓我們來看看另一個因為我們代碼中的 bug 引起的別的庫中 panic! 的例子,而不是直接的巨集調用。
嘗試訪問超越 vector 結尾的元素,這會造成 panic!:

fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

可以設置 RUST_BACKTRACE 環境變數來得到一個 backtrace,backtrace 是一個執行到目前位置所有被調用的函數的列表。Rust 的 backtrace 跟其他語言中的一樣:閱讀 backtrace 的關鍵是從頭開始讀直到發現你編寫的文件。

將 RUST_BACKTRACE 環境變數設置為任何不是 0 的值來獲取 backtrace 看看:

$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14
   2: core::panicking::panic_bounds_check
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9
   6: panic::main
             at ./src/main.rs:4:5
   7: core::ops::function::FnOnce::call_once
             at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

為了獲取帶有這些信息的 backtrace,必須啟用 debug 標識。當不使用 --release 參數運行 cargo build 或 cargo run 時 debug 標識會預設啟用,就像這裡一樣。

Windows設置 RUST_BACKTRACE 環境變數的兩種方式

在cmd中執行

set RUST_BACKTRACE=1

在powershell中執行:

$env:RUST_BACKTRACE=1 ; cargo run

用 Result 處理可恢復的錯誤

使用 Result 類型來處理潛在的錯誤中的那個 Result 枚舉,它定義有Ok 和 Err兩個成員:

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

T 和 E 是泛型類型參數:

  • T 代表成功時返回的 Ok 成員中的數據的類型
  • E 代表失敗時返回的 Err 成員中的錯誤的類型

調用一個返回 Result 的函數,它打開一個文件,可能會失敗:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");
}

File::open 的返回值是 Result<T, E>:

  • 泛型參數 T 會被 File::open 的實現放入成功返回值的類型 std::fs::File,這是一個文件句柄。
  • 錯誤返回值使用的 E 的類型是 std::io::Error

`File::open`` 調用可能成功並返回一個可以讀寫的文件句柄,也可能會失敗,如文件不存在或無訪問許可權。

需要在代碼中增加根據 File::open 返回值進行不同處理的邏輯:

use std::fs::File;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

如果當前目錄沒有一個叫做 hello.txt 的文件,當運行這段代碼時 panic! 巨集的輸出能告訴了我們出錯的地方。

匹配不同的錯誤

使用不同的方式處理不同類型的錯誤:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file_result = File::open("hello.txt");

    let greeting_file = match greeting_file_result {
        Ok(file) => file,
        Err(error) => match error.kind() {
            //嘗試打開的文件並不存在,通過 File::create 創建文件
            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);
            }
        },
    };
}
  • File::open 返回的 Err 成員中的值類型 io::Error,它是一個標準庫中提供的結構體。
  • io::Error 結構體有一個返回 io::ErrorKind 值的 kind 方法可供調用。
  • io::ErrorKind 是一個標準庫提供的枚舉,它的成員對應 io 操作可能導致的不同錯誤類型。

不同於使用 match 和 Result<T, E>

在處理代碼中的 Result<T, E> 值時,相比於使用 match ,使用閉包和 unwrap_or_else 方法會更加簡潔:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = 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);
        }
    });
}

失敗時 panic 的簡寫:unwrap 和 expect

Result<T, E> 類型定義了很多輔助方法來處理各種情況,其中之一叫做 unwrap:

  • 如果 Result 值是成員 Ok,unwrap 會返回 Ok 中的值。
  • 如果 Result 是成員 Err,unwrap 會為我們調用 panic!。

一個實踐 unwrap 的例子:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

另一個類似於 unwrap 的方法它還允許選擇 panic! 的錯誤信息:expect,語法如下:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

expect 與 unwrap 的使用方式一樣:返迴文件句柄或調用 panic! 巨集。

在生產級別的代碼中,大部分人選擇 expect 而不是 unwrap 並提供更多關於為何操作期望是一直成功的上下文。

傳播錯誤

一個從文件中讀取用戶名的函數,如果文件不存在或不能讀取,這個函數會將這些錯誤返回給調用它的代碼:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),  //最後一個表達式,無需顯式調用 return 語句
    }
}

函數的返回值類型為 Result<String, io::Error>,說明函數返回一個 Result<T, E> 類型的值:泛型參數 T 的具體類型是 String,而 E 的具體類型是 io::Error。

這裡選擇 io::Error 作為函數的返回值是因為它正好是函數體中那兩個可能會失敗的操作的錯誤返回值:File::open 函數和 read_to_string 方法。

傳播錯誤的簡寫:? 運算符

一個使用 ? 運算符向調用者返回錯誤的函數:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

Result 值之後的 ? 被定義為與前面示例中定義的處理 Result 值的 match 表達式有著完全相同的工作方式。不同的是,? 運算符所使用的錯誤值被傳遞給了 from 函數,它定義於標準庫的 From trait 中,其用來將錯誤從一種類型轉換為另一種類型。

問號運算符之後的鏈式方法調用:

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

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

使用 fs::read_to_string 而不是打開後讀取文件

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

哪裡可以使用 ? 運算符

? 運算符只能被用於返回值與 ? 作用的值相相容的函數,因為 ? 運算符被定義為從函數中提早返回一個值,函數的返回值必須是 Result 才能與這個 return 相相容。

嘗試在返回 () 的 main 函數中使用 ? 的代碼不能編譯:

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")?;
}

當編譯這些代碼時會出錯,錯誤指出只能在返回 Result 或者其它實現了 FromResidual 的類型的函數中使用 ? 運算符

為了修複這個錯誤,有兩個選擇。一個是,如果沒有限制的話將函數的返回值改為 Result<T, E>。另一個是使用 match 或 Result<T, E> 的方法中合適的一個來處理 Result<T, E>。

在 Option<T> 上調用 ? 運算符的行為與 Result<T, E> 類似:如果值是 None,此時 None 會從函數中提前返回。如果值是 Some,Some 中的值作為表達式的返回值同時函數繼續

在 Option 值上使用 ? 運算符:

//從給定文本中返回第一行最後一個字元
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

註意你可以在返回 Result 的函數中對 Result 使用 ? 運算符,可以在返回 Option 的函數中對 Option 使用 ? 運算符,但是不可以混合搭配。? 運算符不會自動將 Result 轉化為 Option,反之亦然;在這些情況下,可以使用類似 Result 的 ok 方法或者 Option 的 ok_or 方法來顯式轉換。

修改 main 返回 Result<(), E> 允許對 Result 值使用 ? 運算符:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

目前可以將 Box<dyn Error> 理解為 “任何類型的錯誤”,在返回 Box<dyn Error> 錯誤類型 main 函數中對 Result 使用 ? 是允許的,因為它允許任何 Err 值提前返回。

要不要 panic!

示例、代碼原型和測試都非常適合 panic

調用一個類似 unwrap 這樣可能 panic! 的方法可以被理解為一個你實際希望程式處理錯誤方式的占位符,它根據其餘代碼運行方式可能會各不相同。
在我們準備好決定如何處理錯誤之前,unwrap和expect方法在原型設計時非常方便。當我們準備好讓程式更加健壯時,它們會在代碼中留下清晰的標記。

當我們比編譯器知道更多的情況

當你有一些其他的邏輯來確保 Result 會是 Ok 值時,調用 unwrap 或者 expect 也是合適的,雖然編譯器無法理解這種邏輯:

use std::net::IpAddr;

let home: IpAddr = "127.0.0.1"
    .parse()
    .expect("Hardcoded IP address should be valid");

可以看出 127.0.0.1 是一個有效的 IP 地址,所以這裡使用 expect 是可以接受的。

錯誤處理指導原則

在當有可能會導致有害狀態的情況下建議使用 panic!,有害狀態是指當一些假設、保證、協議或不可變性被打破的狀態(如無效的值、自相矛盾的值或者被傳遞了不存在的值),外加如下幾種情況:

  • 有害狀態是非預期的行為,與偶爾會發生的行為相對,比如用戶輸入了錯誤格式的數據。
  • 在此之後代碼的運行依賴於不處於這種有害狀態,而不是在每一步都檢查是否有問題。
  • 沒有可行的手段來將有害狀態信息編碼進所使用的類型中的情況。

當接收到無效輸入時,優先返回錯誤信息以通知用戶。若繼續執行可能引發安全問題或嚴重後果,則調用 panic! 停止程式並指出bug。同樣,在遇到無法修複的外部代碼無效狀態時,適宜使用 panic!。

當錯誤屬於預期範圍(如解析錯誤或HTTP限流響應),應返回 Result,表明可能出現失敗,並將問題傳遞給調用者處理。此時不宜使用 panic! 來應對這些情況。

當代碼執行操作可能因無效值而危及安全時,應先驗證其有效性,併在無效時 panic!。這是因為嘗試處理此類數據易暴露漏洞,如數組越界訪問會導致 panic! 以防止潛在的安全風險。函數遵循輸入條件的契約,若違反契約則通過 panic! 指出調用方 bug,因為這種錯誤通常無法在函數內部妥善處理,需要程式員修複源代碼。函數契約及其可能導致 panic! 的情況應在 API 文檔中明確說明。

雖然大量錯誤檢查可能冗長繁瑣,但 Rust 的類型系統和編譯器能幫你自動進行許多檢查。若函數參數為非 Option 類型,編譯器確保其非空且有有效值,因此無需在代碼中處理 Some/None 情況。傳遞空值的代碼無法通過編譯,故函數運行時無須判空。同樣,使用如 u32 的無符號整型可確保值永不會為負數。

創建自定義類型進行有效性驗證

使用 if 表達式檢查值是否超出範圍(代碼冗餘、可能影響性能):

loop {
    // --snip--
    let guess: i32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };
    if guess < 1 || guess > 100 {
        println!("The secret number will be between 1 and 100.");
        continue;
    }
    match guess.cmp(&secret_number) {
        // --snip--
}

可以創建一個新類型來將驗證放入創建其實例的函數中,而不是到處重覆這些檢查。這樣就可以安全地在函數簽名中使用新類型並相信它們接收到的值。

一個 Guess 類型,它只在值位於 1 和 100 之間時才繼續:

pub struct Guess {
    //私有的欄位,確保了不會存在沒有通過 Guess::new 函數的條件檢查的 value 
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }

    //有時被稱為 getter,目的就是返回對應欄位的數據
    pub fn value(&self) -> i32 {
        self.value
    }
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 翻轉圖像是在視覺上比較兩個不同圖像的常用方法。單擊其中一個將翻轉它,並顯示另一個圖像。 佈局 佈局結構如下: <div class="flipping-images"> <div class="flipping-images__inner ...
  • 一、實現方案 單獨貼代碼可能容易混亂,所以這裡只講實現思路,代碼放在最後彙總了下。 想要實現一個簡單的工業園區、主要包含的內容是一個大樓、左右兩片停車位、四條道路以及多個可在道路上隨機移動的車輛、遇到停車位時隨機選擇是否要停車,簡單設計圖如下 二、實現步奏 2.1 引入環境,天空和地面 引入天空有三 ...
  • 前言 有時出現的線上bug在測試環境死活都不能復現,靠review代碼猜測bug出現的原因,然後盲改代碼直接線上上測試明顯不靠譜。這時我們就需要在生產環境中debug代碼,快速找到bug的原因,然後將鍋丟出去。 生產環境的代碼一般都是關閉source map和經過混淆的,那麼如何進行debug代碼呢 ...
  • 在接入小程式過程中會遇到需要將 H5 頁面集成到小程式中情況,今天我們就來聊一聊怎麼把 H5 頁面塞到小程式中。 本篇文章將會從下麵這幾個方面來介紹: 小程式承載頁面的前期準備 小程式如何承載 H5 小程式和 H5 頁面如何通訊 小程式和 H5 頁面的相互跳轉 小程式承載頁面的前期準備 首先介紹下我 ...
  • Vue3 對 diff 過程進行了大升級,利用 最長遞增子序列演算法 去計算最少移動dom,儘可能少的做移動節點位置操作! ...
  • 在當今的互聯網時代,微服務架構已經成為許多企業選擇的架構模式,它能夠提高系統的靈活性、可維護性和可擴展性。然而,微服務架構下的高可用性和彈性擴展是一個複雜的挑戰。本文將介紹如何利用容器與中間件來實現微服務架構下的高可用性和彈性擴展的解決方案。 ...
  • 嗨,大家好!歡迎來到C-Shopping,這是一場揭開科技面紗的電商之旅。我是C-Shopping開源作者“繼小鵬”,今天將為你介紹一款基於最新技術的開源電商平臺。讓我們一同探索吧! 點擊 這裡,http://shop.huanghanlian.com/,即刻踏上C-Shopping 體驗之旅! 項 ...
  • 小北說在前面: 在一線互聯網企業種,如網易、美團、位元組、如阿裡、滴滴、極兔、有贊、希音、百度、美團等大廠,資料庫的面試題,一直是核心和重點的提問點,比如前段時間有位小伙伴面試位元組,就遇到了下麵這道面試題: 索引的設計規範,你知道那些? 小伙伴雖然用過索引,但是索引的設計規範忘記得一干二凈,回答也是朦 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...