Rust 的運算元與表示式系統 largely 繼承自 C 語言,但也加入了一些獨特的設計。例如,Rust 使用 ! 作為位元 NOT 運算元,而非 C 語言的 ~。在位元移位方面,Rust 採用符號擴充套件和零擴充套件策略,簡化了無符號整數的處理。型別轉換在 Rust 中通常需要顯式地使用 as 關鍵字,以確保型別安全。閉包作為輕量級的函式式值,提供了更靈活的程式設計方式。Rust 的錯誤處理機制則主要仰賴 panicResult 兩種方式。panic 主要用於處理不可還原的錯誤,例如陣列越界等邏輯錯誤,觸發時會展開堆積疊並清理資源。而 Result 則用於處理可還原的錯誤,例如檔案讀取失敗等,允許程式根據不同的結果執行不同的邏輯。

Rust 的運算元與表示式

Rust 繼承了 C 語言的位元運算元,包括 &, |, ^, <<, 和 >>,但使用 ! 而非 ~ 進行位元 NOT 運算。

let hi: u8 = 0xe0;
let lo = !hi; // 結果為 0x1f

這表示不能使用 !n 來檢查整數 n 是否為零,而應該寫成 n == 0

位元運算的特性

  • 位元移位運算在有符號整數型別上永遠是符號擴充套件,而在無符號整數型別上則是零擴充套件。
  • 由於 Rust 支援無符號整數,因此不需要像 Java 那樣的 >>> 運算元。

比較運算元與邏輯運算元

Rust 的比較運算是 ==, !=, <, <=, >, 和 >=。被比較的兩個值必須具有相同的型別。

邏輯運算元 &&|| 為短路運算,運算元必須是 bool 型別。

指定運算

使用 = 運算元可以對 mut 變數及其欄位或元素進行指定。Rust 也支援複合指定運算,例如:

total += item.price;

等同於:

total = total + item.price;

內容解密:

  • 複合指定運算是簡化程式碼的常見方式,能夠提升可讀性。
  • Rust 不支援像 C 語言那樣的鏈式指定,例如 a = b = 3
  • Rust 沒有 C 語言中的遞增和遞減運算元 ++--

型別轉換

Rust 中,值從一種型別轉換到另一種通常需要顯式轉換。使用 as 關鍵字進行轉換:

let x = 17; // x 是 i32 型別
let index = x as usize; // 轉換為 usize 型別

允許的轉換型別:

  1. 數字之間的轉換:將整數轉換為另一種整數型別是定義良好的,窄化會導致截斷,有符號整數擴充套件是符號擴充套件,無符號整數則是零擴充套件。

    let a: i32 = 256;
    let b = a as u8; // b 將會是 0,因為截斷了高位元組
    

    內容解密:

    • 將大浮點數轉換為過小的整數型別可能會導致未定義行為,這是編譯器的已知問題。
  2. bool, char, 或類別 C 列舉型別 可以轉換為任何整數型別。

    let flag: bool = true;
    let num = flag as u8; // num 將會是 1
    

    內容解密:

    • 反向轉換(例如將 u16 轉換為 char)是不允許的,因為需要執行時檢查來確保值的有效性。
  3. Deref 自動轉換

    • &String 可以自動轉換為 &str
    • &Vec<i32> 可以自動轉換為 &[i32]
    • 這類別轉換被稱為 Deref 自動轉換,適用於實作了 Deref 特性的型別。

閉包(Closures)

Rust 中的閉包是一種輕量級的函式式值。閉包通常由引數列表(用豎線包圍)和表示式組成:

let is_even = |x| x % 2 == 0;

Rust 可以推斷引數和傳回值的型別,也可以顯式指定它們:

let is_even = |x: u64| -> bool { x % 2 == 0 };

呼叫閉包的語法與呼叫函式相同:

assert_eq!(is_even(14), true);

內容解密:

  • 閉包是 Rust 中非常有用的特性,將在第14章深入討論。

表示式的優先順序與結合性

Rust 的表示式語法總結於表6-1中,運算元的優先順序從高到低列出。Rust 的運算元優先順序規則決定了多個相鄰運算元在表示式中的執行順序。

表6-1:表示式型別與相關特性

表示式型別範例相關特性
陣列字面量[1, 2, 3]-
重複陣列字面量[0; 50]-
元組(6, "crullers")-
分組(2 + 2)-
程式區塊{ f(); g() }-
控制流程表達式if ok { f() }-
方法呼叫point.translate(50, 50)Deref, DerefMut
邏輯/位元 NOT!okNot
取反-numNeg
解參照*ptrDeref, DerefMut

內容解密:

  • 該表格提供了 Rust 中各種表示式的語法和相關特性,能夠幫助開發者理解不同表示式的用法和優先順序。

Rust 的錯誤處理機制

Rust 的錯誤處理是其語言特性中相當獨特的一部分,主要分為兩種形式:panicResult。本章節將探討這兩種錯誤處理方式,並分析其應用場景和技術細節。

Panic:程式邏輯錯誤的處理

當程式遇到不可還原的錯誤時,例如陣列越界、除以零等,Rust 會觸發 panic。這類別錯誤通常是由程式自身的邏輯缺陷引起的。panic 發生時,Rust 預設會進行堆積疊展開(unwinding),清理當前的函式呼叫堆積疊,並釋放相關資源。

Panic 的觸發條件

  • 陣列越界存取
  • 整數除以零
  • NoneOption 呼叫 .unwrap()
  • 斷言失敗
fn pirate_share(total: u64, crew_size: usize) -> u64 {
    let half = total / 2;
    half / crew_size as u64
}

fn main() {
    println!("{}", pirate_share(100, 0)); // 將觸發 panic
}

內容解密:

  1. pirate_share 函式計算海盜的分配額度。
  2. crew_size 為 0 時,會觸發除以零的錯誤,導致 panic
  3. panic 發生時,會列印錯誤訊息並展開堆積疊。

Result:可還原錯誤的處理

對於可預見的錯誤,例如檔案讀取失敗或網路連線問題,Rust 使用 Result 來處理。Result 是一個列舉型別,包含 Ok(value)Err(error) 兩種可能狀態。

Result 的基本用法

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => println!("檔案開啟成功: {:?}", file),
        Err(err) => println!("檔案開啟失敗: {}", err),
    }
}

內容解密:

  1. File::open 傳回一個 Result 型別,表示檔案開啟的結果。
  2. 使用 match 陳述式處理 Result,根據不同的狀態執行不同的邏輯。
  3. 若傳回 Ok,表示檔案開啟成功;若傳回 Err,則表示開啟失敗並包含錯誤資訊。

錯誤處理的最佳實踐

  1. 避免濫用 panic:只在程式邏輯確實錯誤時使用 panic,對於可預見的錯誤應使用 Result 處理。
  2. 善用 Result? 運算元:簡化錯誤傳遞的程式碼邏輯,提高可讀性。
  3. 自定義錯誤型別:根據具體需求定義合適的錯誤型別,提高程式的可維護性。

Rust 中的錯誤處理:Panic 與 Result

Rust 語言以其對錯誤處理的嚴謹態度而聞名。在 Rust 中,錯誤處理主要透過兩種機制來實作:panicResult。本篇文章將探討這兩種機制的工作原理、使用場景以及最佳實踐。

Panic:安全的錯誤處理機制

panic 在 Rust 中是一種特殊的錯誤處理機制。當程式遇到無法繼續執行的錯誤時,例如陣列越界存取,Rust 會觸發 panic。與其他語言中的「當機」或「未定義行為」不同,Rust 的 panic 是安全且定義良好的。

Panic 的特點

  • 安全性panic 不會違反 Rust 的安全規則。即使在標準函式庫方法中發生 panic,也不會留下懸掛指標或半初始化的值。
  • 執行緒獨立panic 是按執行緒發生的,一個執行緒的 panic 不會影響其他執行緒的正常執行。
  • 自定義行為:可以透過編譯選項 -C panic=abort 自定義 panic 行為,使程式在第一次 panic 時立即終止。

處理 Panic

雖然 Rust 程式不強制要求處理 panic,但可以使用 std::panic::catch_unwind() 函式捕捉堆積疊展開,從而使執行緒在發生 panic 後繼續執行。這對於提高程式的健壯性非常有用。

Result:明確的錯誤處理

Rust 不使用異常,而是透過 Result 型別來表示可能失敗的操作。Result 是一種列舉型別,定義如下:

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

使用 Result 處理錯誤

呼叫可能失敗的函式時,Rust 要求對傳回的 Result 進行處理。可以使用 match 表示式來處理 Result

match get_weather(hometown) {
    Ok(report) => display_weather(hometown, &report),
    Err(err) => {
        println!("error querying the weather: {}", err);
        schedule_weather_retry();
    }
}

Result 的便捷方法

Result 提供了多種便捷方法來簡化錯誤處理:

  • is_ok()is_err():檢查 Result 是否為成功或錯誤。
  • ok()err():將 Result 轉換為 Option 型別。
  • unwrap_or()unwrap_or_else():在錯誤時傳回預設值或計算預設值。
  • unwrap()expect():在錯誤時觸發 panic
  • as_ref()as_mut():借用 Result 中的值。

示例:使用 unwrap_or 處理預設值

const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);
let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
display_weather(los_angeles, &report);

Rust 中的錯誤處理:探討與實踐

在軟體開發過程中,錯誤處理是一項至關重要的任務。Rust 語言透過其獨特的所有權系統和錯誤處理機制,為開發者提供了強大的工具來處理程式中可能出現的錯誤。本篇文章將探討 Rust 中的 Result 型別、錯誤傳播、以及如何有效地處理和列印錯誤訊息。

使用 Result 型別進行錯誤處理

Rust 中的 Result 型別是一個列舉,用於表示操作的結果。它有兩個可能的值:Ok(value) 表示操作成功,Err(error) 表示操作失敗。Result 型別是處理錯誤的基礎。

Result 型別別名

在 Rust 的檔案中,有時會看到省略了 Result 錯誤型別的情況,如 fn remove_file(path: &Path) -> Result<()>。這通常是因為使用了 Result 型別別名。型別別名是一種簡化型別名稱的方式。例如,std::io 模組定義了一個 Result 型別別名:

pub type Result<T> = result::Result<T, Error>;

這使得 std::io::Result<T> 成為 Result<T, io::Error> 的簡寫。

列印錯誤訊息

當錯誤發生時,將錯誤訊息列印到終端是一種常見的處理方式。Rust 的標準函式庫定義了多種錯誤型別,如 std::io::Errorstd::fmt::Error 等,它們都實作了 std::error::Error 特徵。這意味著它們都可以使用 println! 宏來列印錯誤訊息。

println!("error: {}", err);  // 簡要的錯誤訊息
println!("error: {:?}", err); // 詳細的 Debug 檢視

自定義列印錯誤訊息的函式

標準函式庫的錯誤型別不包含堆積疊追蹤資訊,但可以使用 error-chain crate 自定義錯誤型別以支援堆積疊追蹤。下面是一個自定義的列印錯誤訊息的函式:

use std::error::Error;
use std::io::{Write, stderr};

fn print_error(mut err: &Error) {
    let _ = writeln!(stderr(), "error: {}", err);
    while let Some(cause) = err.cause() {
        let _ = writeln!(stderr(), "caused by: {}", cause);
        err = cause;
    }
}

錯誤傳播

在大多數情況下,當某個操作可能失敗時,我們不希望立即捕捉和處理錯誤。相反,我們希望將錯誤傳播給呼叫者。Rust 的 ? 運算子使得這變得簡單。

let weather = get_weather(hometown)?;

? 運算子根據函式傳回的結果進行不同的操作:如果成功,則解封裝 Result 取得內部的成功值;如果失敗,則立即從封閉函式傳回,將錯誤結果傳播給呼叫鏈。

等價的 match 表示式

? 運算子的功能可以使用 match 表示式來實作,雖然這樣寫起來更冗長:

let weather = match get_weather(hometown) {
    Ok(success_value) => success_value,
    Err(err) => return Err(err)
};
程式碼範例:使用 ? 運算子處理錯誤
use std::fs;
use std::io;
use std::path::Path;

fn read_file(filename: &Path) -> io::Result<String> {
    let mut file = fs::File::open(filename)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

內容解密:

  1. fs::File::open(filename)?;:嘗試開啟檔案。如果失敗,則立即傳回 Err(io::Error)
  2. file.read_to_string(&mut contents)?;:讀取檔案內容到字串。如果失敗,則立即傳回 Err(io::Error)
  3. Ok(contents):如果所有操作都成功,則傳回包含檔案內容的字串。

這個例子展示瞭如何使用 ? 運算子簡化錯誤處理邏輯,使程式碼更加簡潔和易於閱讀。

處理多重錯誤型別

在實際的程式設計中,我們經常會遇到多種錯誤型別。假設我們要從一個文字檔中讀取數字。

讀取檔案中的數字

use std::io::{self, BufRead};

/// 從文字檔中讀取整數。
/// 檔案應該每行包含一個數字。
fn read_numbers(file: &mut BufRead) -> Result<Vec<i64>, io::Error> {
    let mut numbers = vec![];
    for line_result in file.lines() {
        let line = line_result?; // 讀取行可能會失敗
        numbers.push(line.parse()?); // 解析整數可能會失敗
    }
    Ok(numbers)
}

Rust 編譯器會報錯:

numbers.push(line.parse()?); // 解析整數可能會失敗
^^^^^^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>`
is not implemented for `std::io::Error`

這是因為 line_resultline.parse() 會產生不同的錯誤型別。line_result 的型別是 Result<String, std::io::Error>,而 line.parse() 的型別是 Result<i64, std::num::ParseIntError>。我們的 read_numbers() 函式只支援 io::Error 錯誤型別。

使用通用錯誤型別

一個簡單的解決方法是使用 Rust 內建的通用錯誤型別。所有的標準函式庫錯誤型別都可以轉換成 Box<std::error::Error> 型別,代表「任何錯誤」。我們可以定義以下型別別名:

type GenError = Box<std::error::Error>;
type GenResult<T> = Result<T, GenError>;

然後,將 read_numbers() 的傳回型別改為 GenResult<Vec<i64>>。這樣,函式就可以編譯成功。? 運算元會自動將錯誤型別轉換成 GenError

範例程式碼

fn read_numbers(file: &mut BufRead) -> GenResult<Vec<i64>> {
    let mut numbers = vec![];
    for line_result in file.lines() {
        let line = line_result?; 
        numbers.push(line.parse()?); 
    }
    Ok(numbers)
}

內容解密:

  1. 通用錯誤型別的定義:使用 type GenError = Box<std::error::Error>; 定義一個可以代表任何錯誤的型別。
  2. ? 運算元的自動轉換:在 read_numbers 函式中,? 運算元會自動將 io::ErrorParseIntError 轉換成 GenError
  3. 傳回型別的變更:將函式的傳回型別變更為 GenResult<Vec<i64>> 以支援多種錯誤型別。

使用 error.downcast_ref 處理特定錯誤

如果我們想要處理特定的錯誤型別,但讓其他錯誤繼續傳播,可以使用 error.downcast_ref::<ErrorType>() 方法:

loop {
    match compile_project() {
        Ok(()) => return Ok(()),
        Err(err) => {
            if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
                insert_semicolon_in_source_code(mse.file(), mse.line())?;
                continue; // 再試一次!
            }
            return Err(err);
        }
    }
}

內容解密:

  1. error.downcast_ref 的使用:該方法允許我們檢查錯誤是否屬於特定的型別。
  2. 特定錯誤的處理:如果錯誤是 MissingSemicolonError 型別,我們會插入分號並繼續。
  3. 其他錯誤的傳播:如果不是該特定錯誤,則繼續傳播錯誤。

處理「不可能發生的錯誤」

有時我們知道某個錯誤不可能發生。例如,當我們解析一個字串為數字時,如果我們已經確認該字串只包含數字,那麼解析失敗的可能性就不存在了。

使用 .unwrap() 處理不可能發生的錯誤

let num = digits.parse::<u64>().unwrap();

內容解密:

  1. .unwrap() 的作用:如果結果是 Ok,則傳回內部的數值;如果是 Err,則 panic。
  2. 使用場景:當我們確定錯誤不可能發生時,可以使用 .unwrap()
  3. 風險:如果我們的假設是錯誤的,使用 .unwrap() 可能會導致程式 panic。