Rust 的區塊和分號在程式碼執行流程中扮演著關鍵角色。區塊作為表示式,其值由最後一個表示式決定,分號則用於結束表示式陳述式並捨棄其值。這與 C 語言的區塊運作方式不同,需要特別注意避免錯誤。Rust 提供了豐富的控制流程表達式,例如條件表示式 if、匹配表示式 match,以及 if letwhile let 等簡化模式匹配的語法糖,讓程式碼更具彈性。迴圈表示式包含 forwhileloop 等,配合 breakcontinue 可以精細控制迴圈流程,搭配生命週期標籤更能精確跳出或繼續特定迴圈。錯誤處理方面,? 運算元簡化了錯誤傳播流程,提升程式碼可讀性。loop 表示式則明確標示無限迴圈,有助於編譯器進行流程分析。發散函式以 ! 型別標記,表示永遠不會正常傳回。函式和方法呼叫語法簡潔,. 運算元自動處理參照和智慧指標。最後,文章詳細介紹了 Rust 的運算元,包含方法呼叫、欄位和元素存取、左值表示式、切片提取、範圍運算元等,並以快速排序範例展示了這些語法元素的實際應用。

程式區塊和分號

程式區塊也是表示式。一個區塊會產生一個值,可以在任何需要值的地方使用:

let display_name = match post.author() {
    Some(author) => author.name(),
    None => {
        let network_info = post.get_network_metadata()?;
        let ip = network_info.client_address();
        ip.to_string()
    }
};

內容解密:

  • match 表示式處理 post.author() 的不同傳回值。
  • 如果傳回值是 Some(author),則直接傳回 author.name()
  • 如果傳回值是 None,則執行區塊中的程式碼,最後傳回 ip.to_string()

區塊中的最後一個表示式 ip.to_string() 就是該區塊的值。請注意,該表示式後面沒有分號。大多數 Rust 程式碼行都以分號或大括號結尾,就像 C 或 Java 一樣。如果一個區塊看起來像 C 程式碼,在所有熟悉的地方都有分號,那麼它將像 C 區塊一樣執行,其值將是 ()。正如我們在第 2 章中提到的,當你省略最後一個表示式的分號時,區塊就會傳回該表示式的值。

Rust 程式語言中的區塊與分號使用解析

在 Rust 程式語言中,區塊(block)是一個由大括號 {} 包圍的程式碼區域,可以用來組織程式碼、控制變數的作用域,以及作為表示式來產生值。區塊的使用與分號的處理息息相關,這是 Rust 語法中的一個重要特點。

區塊作為表示式

在 Rust 中,區塊可以作為表示式使用,這意味著它可以產生一個值。當區塊作為表示式時,其最後一個表示式的值會被當作整個區塊的值傳回。例如:

let msg = {
    let dandelion_control = puffball.open();
    dandelion_control.release_all_seeds(launch_codes);
    dandelion_control.get_status()
};

內容解密:

  1. let msg = { ... }; 定義了一個變數 msg,並將區塊的傳回值賦給它。
  2. 在區塊內部,let dandelion_control = puffball.open(); 聲明瞭一個區域性變數 dandelion_control 並初始化。
  3. dandelion_control.release_all_seeds(launch_codes); 呼叫了一個方法,但因為後面有分號,所以其傳回值被丟棄。
  4. dandelion_control.get_status() 呼叫了另一個方法,並將其傳回值作為整個區塊的傳回值,因為這行後面沒有分號。

分號的作用

在 Rust 中,分號用來結束一個表示式陳述式。如果一個表示式後面跟著分號,其傳回值會被丟棄。例如:

dandelion_control.release_all_seeds(launch_codes); // 傳回值被丟棄

如果省略了分號,Rust 會將該表示式的值作為區塊的傳回值。因此,如果不小心省略了分號,可能會導致編譯錯誤或意外的行為。

錯誤處理:缺少分號

當你不小心省略了分號,Rust 編譯器可能會給出令人困惑的錯誤訊息。例如:

if preferences.changed() {
    page.compute_size() // 錯誤:缺少分號
}

編譯器錯誤訊息可能如下:

error[E0308]: mismatched types
--> expressions_missing_semicolon.rs:19:9
|
19 | page.compute_size() // oops, missing semicolon
| ^^^^^^^^^^^^^^^^^^^ expected (), found tuple
|
= note: expected type `()`
found type `(u32, u32)`

內容解密:

  1. 編譯器預期 page.compute_size() 後面應該有分號,因此其傳回值應該是 ()(表示沒有傳回值)。
  2. 但實際上 page.compute_size() 傳回了一個 (u32, u32) 型別的 tuple,因此導致了型別不匹配的錯誤。

空白陳述式

Rust 允許空白陳述式,即單獨的一個分號。這種陳述式不執行任何操作,通常不會被使用。

loop {
    work();
    play();
    ; // 空白陳述式
}

內容解密:

  1. ; 在這裡是一個空白陳述式,不執行任何操作。
  2. 這種寫法通常不被推薦,因為它可能會讓程式碼的可讀性變差。

宣告

區塊內除了表示式和分號外,還可以包含宣告,如 let 宣告用於宣告區域性變數。

let name;
if user.has_nickname() {
    name = user.nickname();
} else {
    name = generate_unique_name();
    user.register(&name);
}

內容解密:

  1. let name; 聲明瞭一個變數 name 但沒有初始化。
  2. 在條件判斷中,根據不同的條件對 name 進行初始化。
  3. 這種寫法有助於在不同的條件下對變數進行初始化,但必須確保變數在使用前已經被初始化。

Rust 控制流程表達式詳解

Rust 語言提供豐富的控制流程表達式,包括條件表示式、匹配表示式和迴圈表示式等,這些表示式讓開發者能夠編寫出更具彈性和效率的程式碼。本章節將探討這些控制流程表達式的使用方法和注意事項。

條件表示式(if 表示式)

Rust 中的 if 表示式用於根據條件執行不同的程式碼區塊。與 C 語言不同,Rust 的 if 表示式不需要在條件周圍加上括號,但需要使用大括號來包圍程式碼區塊。

if condition {
    // 程式碼區塊
} else if another_condition {
    // 另一個程式碼區塊
} else {
    // 預設程式碼區塊
}
  • 所有分支的傳回值型別必須相同。
  • if 表示式可以作為指定陳述式的一部分。

程式碼範例

let suggested_pet = if with_wings { Pet::Buzzard } else { Pet::Hyena };

內容解密:

  • 上述範例展示瞭如何使用 if 表示式根據條件指定給變數。
  • with_wingstrue 時,suggested_pet 被指定為 Pet::Buzzard,否則為 Pet::Hyena
  • 這種用法簡潔明瞭,並且所有分支的傳回值型別必須相同。

匹配表示式(match 表示式)

match 表示式類別似於 C 語言中的 switch 陳述式,但更為靈活。Rust 的 match 可以匹配多種模式,並且支援複雜的模式匹配。

match value {
    pattern1 => expr1,
    pattern2 => expr2,
    _ => default_expr,
}
  • match 表示式會逐一檢查給定的值是否符合各個模式。
  • 當某個模式匹配成功時,對應的表示式會被執行,並且 match 表示式結束。
  • 至少要有一個模式能夠匹配所有可能的值,否則會導致編譯錯誤。

程式碼範例

match code {
    0 => println!("OK"),
    1 => println!("Wires Tangled"),
    2 => println!("User Asleep"),
    _ => println!("Unrecognized Error {}", code),
}

內容解密:

  • 上述範例展示瞭如何使用 match 表示式根據 code 的值輸出不同的訊息。
  • _ 萬用字元用於匹配所有未被明確列出的值,作為預設情況處理。
  • 編譯器可以對這種 match 表示式進行最佳化,例如使用跳轉表或陣列存取。

if let 和 while let 表示式

if letwhile let 是用於簡化模式匹配的語法糖。它們允許在條件成立時執行特定的程式碼區塊。

程式碼範例

if let Some(cookie) = request.session_cookie {
    return restore_session(cookie);
}

while let Some(item) = queue.pop() {
    process_item(item);
}

內容解密:

  • if let 用於檢查一個值是否符合某個模式,如果符合則執行對應的程式碼區塊。
  • while let 類別似於 if let,但會持續執行直到條件不再成立。
  • 這兩種表示式簡化了對 OptionResult 等型別的處理。

迴圈表示式(Loops)

Rust 提供了多種迴圈表示式,包括 whilewhile letloopfor

for 迴圈

for i in 0..20 {
    println!("{}", i);
}

內容解密:

  • for 迴圈用於遍歷集合或範圍。
  • 0..20 建立了一個範圍,從 0 到 19。
  • 在迴圈中,i 會依次取得範圍內的值。

loop 迴圈

loop {
    // 無限迴圈,直到遇到 break 或 return
}

內容解密:

  • loop 用於建立無限迴圈。
  • 可以使用 breakreturn 跳出迴圈。

break 和 continue

  • break 用於立即離開當前迴圈。
  • continue 用於跳過當前迭代,直接進入下一次迴圈。

程式碼範例

for line in input_lines {
    let trimmed = trim_comments_and_whitespace(line);
    if trimmed.is_empty() {
        continue;
    }
    // 處理 trimmed
}

內容解密:

  • 上述範例展示瞭如何使用 continue 跳過空白行。
  • trimmed 為空時,迴圈會直接進入下一次迭代。

Rust 控制流程與函式呼叫深度解析

Rust 語言的控制流程與函式呼叫機制是其程式設計的核心要素。透過精確的語法設計,Rust 提供了強大的錯誤處理和程式流程控制能力。

continue 陳述式的應用與特性

在 Rust 中,continue 陳述式用於控制迴圈流程。在 for 迴圈中,它會推進到集合中的下一個值;在 while 迴圈中,則會重新檢查迴圈條件。值得注意的是,迴圈可以使用生命週期標籤進行標記,如下例所示:

'search:
for room in apartment {
    for spot in room.hiding_spots() {
        if spot.contains(keys) {
            println!("Your keys are {} in the {}.", spot, room);
            break 'search;
        }
    }
}

程式碼解析:

  1. 'search: 是外層 for 迴圈的標籤,用於識別特定的迴圈。
  2. break 'search; 用於跳出標記為 'search 的外層迴圈,而非僅僅跳出內層迴圈。
  3. 此機制同樣適用於 continue 陳述式,可用於控制多層巢狀迴圈的執行流程。

return 表示式的應用與錯誤傳播

return 表示式用於立即離開目前函式並傳回一個值給呼叫者。在錯誤處理中,? 運算元提供了一種簡潔的錯誤傳播機制,等同於一個 match 表示式:

let output = File::create(filename)?;

等同於:

let output = match File::create(filename) {
    Ok(f) => f,
    Err(err) => return Err(err),
};

程式碼解析:

  1. File::create(filename)? 嘗試建立檔案,若成功則傳回檔案控制程式碼;若失敗則立即傳回錯誤。
  2. 這種機制避免了冗長的錯誤處理程式碼,提升了程式碼的可讀性。

為何 Rust 需要 loop

Rust 編譯器進行多項流程敏感性分析,包括:

  • 檢查函式是否在所有路徑上傳回正確型別的值。
  • 驗證區域性變數在使用前是否已初始化。
  • 警告不可達到的程式碼。

這些分析需要簡單而一致的規則,因此 Rust 編譯器假設任何條件都可能為真或假。對於無限迴圈的情況,可以使用 loop 表示式明確表示:

fn wait_for_process(process: &mut Process) -> i32 {
    loop {
        if process.wait() {
            return process.exit_code();
        }
    }
}

程式碼解析:

  1. loop 明確表示這是一個無限迴圈,直到內部條件滿足才會離開。
  2. 這種明確的表示方式幫助編譯器正確理解程式邏輯。

發散函式與 ! 型別

在 Rust 中,某些表示式(如 panic!()std::process::exit())永遠不會正常結束。這些表示式的型別被標記為 !,稱為發散函式。例如:

fn exit(code: i32) -> ! {
    // 永遠不會傳回
}

程式碼解析:

  1. -> ! 表示該函式永遠不會傳回正常值。
  2. 這種標記使得編譯器在進行型別檢查時豁免這些函式的型別匹配規則。

函式與方法呼叫

Rust 中的函式和方法的呼叫語法與多數程式語言相似:

let x = gcd(1302, 462); // 函式呼叫
let room = player.location(); // 方法呼叫

程式碼解析:

  1. . 運算元自動處理參照和智慧指標,使得方法呼叫更加靈活。
  2. 這種設計簡化了參照和值之間的轉換邏輯。

Rust 程式語言中的運算元與表示式

Rust 語言提供了豐富的運算元和表示式來進行各種操作。在本章中,我們將探討 Rust 中的運算元,包括其語法、用法和特點。

方法呼叫

Rust 中的方法呼叫使用 . 運算元。這個運算元會自動解參照或借用參考,如有需要。

let mut numbers = Vec::new(); // 靜態方法呼叫

靜態方法和非靜態方法之間的區別與物件導向語言中的相同:非靜態方法在值上呼叫(如 my_vec.len()),而靜態方法在型別上呼叫(如 Vec::new())。

方法鏈

Rust 允許方法鏈的寫法,使程式碼更加簡潔。

Iron::new(router).http("localhost:3000").unwrap();

泛型方法呼叫

在函式或方法呼叫中,直接使用 Vec<T> 的語法可能會導致錯誤,因為 < 在表示式中被視為小於運算元。Rust 編譯器建議使用 ::<T> 來解決這個問題。

return Vec::<i32>::with_capacity(1000); // 正確,使用 ::<
let ramp = (0 .. n).collect::<Vec<i32>>(); // 正確,使用 ::<

::<...> 被稱為「渦輪魚」(turbofish),是 Rust 社群中的一個有趣術語。

省略型別引數

在許多情況下,Rust 可以推斷出型別引數,因此可以省略它們。

return Vec::with_capacity(10); // 正確,如果函式傳回型別是 Vec<i32>
let ramp: Vec<i32> = (0 .. n).collect(); // 正確,變數的型別已指定

省略型別引數被視為良好的程式風格,只要它們可以被推斷出來。

欄位和元素存取

結構體的欄位可以使用熟悉的語法進行存取。元組也是如此,只是它們的欄位有編號而不是名稱。

game.black_pawns // 結構體欄位
coords.1 // 元組元素

如果點左邊的值是參考或智慧指標型別,它將自動解參照,就像方法呼叫一樣。

陣列、切片和向量元素存取

可以使用方括號來存取陣列、切片或向量的元素。

pieces[i] // 陣列元素

左邊的值將自動解參照。

左值表示式

像上述三個表示式這樣的表示式被稱為左值,因為它們可以出現在指定運算元的左邊。

game.black_pawns = 0x00ff0000_00000000_u64;
coords.1 = 0;
pieces[2] = Some(Piece::new(Black, Knight, coords));

當然,這只有在 gamecoordspieces 被宣告為可變變數時才允許。

切片提取

從陣列或向量中提取切片很簡單。

let second_half = &game_moves[midpoint .. end];

無論 game_moves 是陣列、切片還是向量,結果都是一個長度為 end - midpoint 的借用切片。在 second_half 的生命週期內,game_moves 被視為借用。

範圍運算元

.. 運算元允許任一運算元被省略;根據存在的運算元,它可以產生多達四種不同的物件型別。

.. // RangeFull
a .. // RangeFrom { start: a }
.. b // RangeTo { end: b }
a .. b // Range { start: a, end: b }

Rust 範圍是半開的:它們包含起始值(如果有的話),但不包含結束值。

可迭代範圍

只有包含起始值的範圍才是可迭代的,因為迴圈必須有一個起始點。但是在陣列切片中,所有四種形式都是有用的。如果範圍的起始或結束被省略,它預設為被切片資料的起始或結束。

快速排序範例

快速排序是一種經典的分治排序演算法,其實作可能如下所示:

fn quicksort<T: Ord>(slice: &mut [T]) {
    if slice.len() <= 1 {
        return; // 無需排序。
    }
    // 將切片分割成兩個部分:前半部分和後半部分。
    let pivot_index = partition(slice);
    // 遞迴排序 `slice` 的前半部分。
    quicksort(&mut slice[.. pivot_index]);
    // 以及後半部分。
    quicksort(&mut slice[pivot_index + 1 ..]);
}

參考運算元

一元 * 運算元用於存取參考指向的值。當使用 . 運算元存取欄位或方法時,Rust 自動跟隨參考,因此只有在我們想要讀取或寫入參考指向的整個值時才需要 * 運算元。

解參照範例

let padovan: Vec<u64> = compute_padovan_sequence(n);
for elem in &padovan {
    draw_triangle(turtle, *elem);
}

在這個例子中,elem 的型別是 &u64,所以 *elemu64

算術、位元、比較和邏輯運算元

Rust 的二元運算元與其他許多語言相似。為了節省時間,我們假設熟悉其中一種語言,並重點介紹 Rust 與傳統做法不同的幾個方面。

算術運算元

Rust 具有通常的算術運算元:+-*/%。正如第 3 章提到的,在偵錯建置中會檢測整數溢位並導致 panic。標準函式庫提供了像 a.wrapping_add(b) 這樣的方法來進行未檢查的算術運算。

除零錯誤

將整數除以零即使在釋出版本中也會觸發 panic。整數有一個 a.checked_div(b) 方法,它傳回一個 Option(如果 b 為零則為 None),並且永遠不會 panic。

一元負號

一元 - 對數字進行否定。它支援除無符號整數外的所有數字型別。沒有一元 + 運算元。

println!("{}", -100); // -100
println!("{}", -100u32); // 錯誤:無法對型別 `u32` 套用一元 `-`
println!("{}", +100); // 錯誤:預期表示式,找到 `+`

取模運算

如同 C 語言,a % b 計算除法的餘數或模數。結果具有與左運算元相同的符號。注意 % 可以用於浮點數以及整數。

let x = 1234.567 % 10.0; // 約等於 4.567