Rust 的 Result 型別提供了一種強大的錯誤處理機制,強制開發者明確處理錯誤,避免錯誤被忽略。透過 ? 運算子,可以簡化錯誤傳播,使程式碼更簡潔。在 main 函式中,需要特殊處理錯誤,例如使用 if let Err(err) 或 .expect()。此外,可以定義自定義錯誤型別,並實作 Display 和 std::error::Error 特性,使其更易於使用。Rust 的模組系統則提供了程式碼組織和可見性管理的機制。使用 mod 關鍵字定義模組,pub 關鍵字控制可見性。模組可以巢狀,並可以放在單獨的檔案中,使用 use 陳述式匯入需要的專案,簡化程式碼。Cargo 作為 Rust 的套件管理器,可以管理專案依賴和版本控制,並從 crates.io 下載和編譯所需的 Crate。透過 Cargo.toml 檔案可以組態編譯選項,例如在發布版本中啟用除錯符號。
Rust 中的錯誤處理:探討 Result 型別
Rust 語言透過 Result 型別提供了一種強大且靈活的錯誤處理機制。與其他語言中的例外處理不同,Rust 強制開發者在可能發生錯誤的地方做出明確的決定,並將其記錄在程式碼中。
為什麼使用 Result?
Rust 選擇 Result 而非異常的主要原因在於其設計哲學:
- 強制開發者對錯誤處理做出明確的決定,避免因疏忽而導致錯誤處理不當。
- 使用
?運算子簡化錯誤傳播,使程式碼更為簡潔。 - 函式的傳回型別明確表明其是否可能失敗,提高程式碼的可讀性和可維護性。
- Rust 編譯器會檢查 Result 值的使用,確保錯誤不會被默默忽略。
- Result 是一種普通的資料型別,可以輕鬆地將成功和錯誤結果儲存在同一集合中,便於處理部分成功或失敗的情況。
自定義錯誤型別
在編寫函式庫或框架時,定義自定義錯誤型別是一種良好的實踐。例如,在編寫 JSON 解析器時,可以定義一個 JsonError 結構體來表示特定的錯誤情況:
#[derive(Debug, Clone)]
pub struct JsonError {
pub message: String,
pub line: usize,
pub column: usize,
}
為了使自定義錯誤型別更易於使用,需要實作 Display 和 std::error::Error 特性:
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{} ({}:{})", self.message, self.line, self.column)
}
}
impl std::error::Error for JsonError {
fn description(&self) -> &str {
&self.message
}
}
內容解密:
JsonError結構體定義:此結構體用於表示 JSON 解析過程中可能發生的錯誤,包含錯誤訊息、行號和列號等資訊。Display特性實作:透過實作fmt方法,定義了JsonError例項如何被格式化為字串。這使得錯誤訊息可以被列印預出來,並且包含了行號和列號,有助於除錯。std::error::Error特性實作:實作description方法傳回錯誤訊息的字串切片,這是std::error::Error特性所要求的,使得JsonError可以作為一個標準的錯誤型別被使用。
在 main 函式中處理錯誤
在大多數情況下,錯誤會透過 ? 運算子傳播到呼叫者。然而,當錯誤到達 main 函式時,需要特殊處理,因為 main 不能直接使用 ?。
一種簡單的方法是使用 .expect() 方法:
fn main() {
calculate_tides().expect("error");
}
然而,這種方法會導致 panic,並可能列印預出令人困惑的錯誤訊息。更好的做法是手動處理錯誤並列印自定義的錯誤訊息:
fn main() {
if let Err(err) = calculate_tides() {
print_error(&err);
std::process::exit(1);
}
}
內容解密:
- 使用
.expect()方法:當calculate_tides()傳回錯誤時,.expect()會導致程式 panic,並列印預出指定的錯誤訊息。這種方法簡單直接,但可能導致不友好的錯誤訊息。 - 手動處理錯誤:透過
if let Err(err)陳述式,可以檢查calculate_tides()是否傳回了錯誤。如果有錯誤,則呼叫print_error函式列印預出自定義的錯誤訊息,並透過std::process::exit(1)終止程式,傳回非零的離開碼,表示程式異常終止。
忽略錯誤
有時,我們可能希望完全忽略某些錯誤。在 Rust 中,可以使用 let _ = ... 的慣用法來忽略 Result 值:
let _ = writeln!(stderr(), "error: {}", err);
內容解密:
let _ = ...慣用法:透過將 Result 值賦給_,可以顯式地忽略該值,避免編譯器的未使用變數警告。- 忽略錯誤的場景:在某些情況下,例如在列印錯誤訊息時發生另一個錯誤,我們可能無法或不想處理這個次要的錯誤。此時,使用
let _ = ...可以忽略這個錯誤,讓程式繼續執行。
第8章:Crates與模組
系統程式設計師也能擁有美好的事物。 — Robert O’Callahan,「Rust的隨想:Crates.io與IDE」
假設你正在撰寫一個模擬蕨類別植物生長的程式,從單一細胞到整個植物體的層次。你的程式就像蕨類別一樣,一開始非常簡單,所有的程式碼可能都在一個檔案中——只是一個想法的小小種子。隨著它成長,它將開始具有內部結構。不同的部分將具有不同的功能。它將分支成多個檔案。最終,它可能會涵蓋整個目錄樹。隨著時間的推移,它可能會成為整個軟體生態系統的重要組成部分。
本章將介紹Rust中幫助你保持程式組織化的功能:Crates和模組。我們還將討論隨著專案成長而自然出現的一系列主題,包括如何記錄和測試Rust程式碼、如何消除不想要的編譯器警告、如何使用Cargo管理專案依賴和版本控制、如何在crates.io上發布開源函式庫等等。
Crates
Rust程式是由Crates組成的。每個Crate都是一個Rust專案:包含單一函式庫或可執行檔的所有原始碼,以及任何相關的測試、範例、工具、組態和其他雜項。對於你的蕨類別模擬器,你可能會使用第三方函式庫進行3D圖形、生物資訊學、平行計算等。這些函式庫以Crate的形式分發(見圖8-1)。
圖8-1:Crate及其依賴關係
此圖示顯示了一個Crate及其依賴的其他Crate。
要了解Crate是什麼以及它們如何協同工作,最簡單的方法是使用cargo build命令並加上--verbose旗標來建置一個具有某些依賴關係的現有專案。我們以「平行Mandelbrot程式」為例進行了此操作。結果如下所示:
$ cd mandelbrot
$ cargo clean # 刪除先前編譯的程式碼
$ cargo build --verbose
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading image v0.6.1
Downloading crossbeam v0.2.9
Downloading gif v0.7.0
Downloading png v0.4.2
... (下載和編譯更多Crate)
Compiling png v0.4.2
Running `rustc .../png-0.4.2/src/lib.rs
--crate-name png
--crate-type lib
--extern num=.../libnum-a2e6e61627ca7fe5.rlib
--extern inflate=.../libinflate-331fc425bf167339.rlib
--extern flate2=.../libflate2-857dff75f2932d8a.rlib
...`
Compiling image v0.6.1
Running `rustc .../image-0.6.1/./src/lib.rs
--crate-name image
--crate-type lib
--extern png=.../libpng-16c24f58491a5853.rlib
...`
Compiling mandelbrot v0.1.0 (file://.../mandelbrot)
Running `rustc src/main.rs
--crate-name mandelbrot
--crate-type bin
--extern crossbeam=.../libcrossbeam-ba292320058da7df.rlib
--extern image=.../libimage-254ec48c8f0684f2.rlib
...`
$
我們重新格式化了rustc命令列以提高可讀性,並刪除了與討論無關的許多編譯器選項,用省略號(…)替換它們。
內容解密:
上述命令展示了Cargo如何下載並編譯專案的依賴Crate。首先,Cargo更新了crate登入檔,然後下載了所需的Crate及其依賴項。接著,它編譯了這些Crate,並最終編譯了我們的Mandelbrot程式。在編譯過程中,Cargo使用了--crate-type選項來指定是編譯函式庫還是可執行檔,並使用--extern選項來指定外部Crate的位置。
你可能記得,到我們完成時,Mandelbrot程式的main.rs包含了三個extern crate宣告:
extern crate num;
extern crate image;
extern crate crossbeam;
這些宣告告訴Rust,num、image和crossbeam是外部函式庫,而不是Mandelbrot程式本身的一部分。
我們還在Cargo.toml檔案中指定了我們想要的每個Crate的版本:
[dependencies]
num = "0.1.27"
image = "0.6.1"
crossbeam = "0.2.8"
這裡的「依賴關係」指的是這個專案使用的其他Crate:我們所依賴的程式碼。我們在crates.io(Rust社群的開源Crate網站)上找到了這些Crate。例如,我們透過存取crates.io並搜尋影像函式庫來瞭解了image函式庫。每個Crate在crates.io上的頁面都提供了檔案和原始碼的連結,以及一行組態,如image = "0.6.1",你可以複製並新增到你的Cargo.toml中。這裡顯示的版本號只是我們撰寫程式時這三個套件的最新版本。
Cargo的輸出記錄了這些資訊如何被使用。當我們執行cargo build時,Cargo首先從crates.io下載指定版本的這些Crate的原始碼。然後,它讀取這些Crate的Cargo.toml檔案,下載它們的依賴項,依此類別推。例如,版本0.6.1的imageCrate的原始碼包含一個Cargo.toml檔案,其中包括以下內容:
[dependencies]
byteorder = "0.4.0"
num = "0.1.27"
enum_primitive = "0.1.0"
glob = "0.2.10"
看到這一點,Cargo知道在它可以使用image之前,必須取得這些Crate。一旦取得了所有原始碼,Cargo就會編譯所有Crate。它為專案依賴圖中的每個Crate執行一次rustc(Rust編譯器)。在編譯函式庫時,Cargo使用--crate-type lib選項。這告訴rustc不要尋找main()函式,而是產生一個.rlib檔案,其中包含以後續rustc命令可以使用的形式編譯的程式碼。在編譯程式時,Cargo使用--crate-type bin,結果是目標平台的二進位制可執行檔:例如,在Windows上的mandelbrot.exe。
內容解密:
Cargo使用不同的組態設定來控制編譯行為。在開發過程中,我們通常使用cargo build來快速編譯和測試程式。然而,當我們準備發布程式時,我們會使用cargo build --release來產生最佳化過的可執行檔。這兩種模式下,Cargo會根據我們的設定調整編譯選項。
建置組態
在你的 Cargo.toml 檔案中有幾個組態設定會影響到 cargo 產生的 rustc 命令列。
| 命令列 | 使用的 Cargo.toml 區段 |
|---|---|
cargo build | [profile.debug] |
cargo build --release | [profile.release] |
cargo test | [profile.test] |
預設值通常就足夠了,但我們發現的一個例外是當你想使用效能分析工具(用於測量你的程式在哪些地方花費了CPU時間)。要從效能分析工具中獲得最佳資料,你需要同時啟用最佳化和除錯符號(通常只在發布版本中啟用最佳化,而只在除錯版本中啟用除錯符號)。要同時啟用兩者,請將以下內容新增到你的 Cargo.toml:
[profile.release]
debug = true # 在發布版本中啟用除錯符號
內容解密:
這段組態允許你在發布版本中同時擁有最佳化和除錯符號,從而能夠更有效地進行效能分析。預設情況下,發布版本只包含最佳化的二進位制檔案,而不包含除錯資訊。透過新增 debug = true,你可以在保持最佳化的同時,也保留除錯符號,這對於理解程式執行時的行為至關重要。
Rust 中的模組系統
Rust 的模組系統是用於組織程式碼和管理可見性的重要工具。模組可以包含函式、型別、常數等,並控制它們的可見性。
模組的基本結構
一個基本的模組定義如下:
mod spores {
use cells::Cell;
/// 一個由成年蕨類別植物產生的細胞,作為蕨類別生命週期的一部分在風中傳播。
pub struct Spore {
// ...
}
/// 模擬透過減數分裂產生孢子的過程。
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
// ...
}
/// 混合基因以準備減數分裂(間期的部分)。
fn recombine(parent: &mut Cell) {
// ...
}
// ...
}
在這個例子中,spores 模組包含了一個公開的 Spore 結構體和兩個函式:produce_spore 和 recombine。其中,produce_spore 是公開的,而 recombine 是私有的。
模組的可見性
Rust 中的模組具有嚴格的可見性控制。使用 pub 關鍵字可以將專案標記為公開,使其可以從模組外部存取。未標記為 pub 的專案則是私有的,只能在模組內部存取。
let s = spores::produce_spore(&mut factory); // 可以存取公開的 produce_spore 函式
spores::recombine(&mut cell); // 錯誤:recombine 是私有的
模組的巢狀結構
模組可以巢狀,建立一個層次結構。這使得程式碼的組織更加清晰。
mod plant_structures {
pub mod roots {
// ...
}
pub mod stems {
// ...
}
pub mod leaves {
// ...
}
}
將模組放在單獨的檔案中
Rust 允許將模組放在單獨的檔案中。例如,可以將 spores 模組放在 spores.rs 檔案中:
// spores.rs
/// 一個由成年蕨類別植物產生的細胞,作為蕨類別生命週期的一部分在風中傳播。
pub struct Spore {
// ...
}
/// 模擬透過減數分裂產生孢子的過程。
pub fn produce_spore(factory: &mut Sporangium) -> Spore {
// ...
}
/// 混合基因以準備減數分裂(間期的部分)。
fn recombine(parent: &mut Cell) {
// ...
}
然後在主檔案中宣告該模組:
mod spores;
路徑和匯入
Rust 使用 :: 運算子來存取模組中的專案。可以使用絕對路徑來參照標準函式庫或其他模組中的專案。
if s1 > s2 {
::std::mem::swap(&mut s1, &mut s2);
}
為了避免重複輸入長路徑,可以使用 use 陳述式匯入需要的專案:
use std::mem;
if s1 > s2 {
mem::swap(&mut s1, &mut s2);
}
多重匯入
可以一次性匯入多個專案:
use std::collections::{HashMap, HashSet}; // 匯入兩個專案
use std::io::prelude::*; // 匯入所有公開專案
這等同於寫出多個單獨的 use 陳述式:
use std::collections::HashMap;
use std::collections::HashSet;
// 標準函式庫中 std::io::prelude 模組的所有公開專案
use std::io::prelude::Read;
use std::io::prelude::Write;
use std::io::prelude::BufRead;
use std::io::prelude::Seek;
從父模組匯入
子模組不會自動繼承父模組中的名稱,需要顯式匯入:
// proteins/synthesis.rs
use super::AminoAcid; // 從父模組匯入 AminoAcid
pub fn synthesize(seq: &[AminoAcid]) // 現在可以正確使用 AminoAcid
// ...
}
使用Plantuml圖表說明模組關係
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 錯誤處理與模組系統解析
package "Rust 記憶體管理" {
package "所有權系統" {
component [Owner] as owner
component [Borrower &T] as borrow
component [Mutable &mut T] as mutborrow
}
package "生命週期" {
component [Lifetime 'a] as lifetime
component [Static 'static] as static_lt
}
package "智慧指標" {
component [Box<T>] as box
component [Rc<T>] as rc
component [Arc<T>] as arc
component [RefCell<T>] as refcell
}
}
package "記憶體區域" {
component [Stack] as stack
component [Heap] as heap
}
owner --> borrow : 不可變借用
owner --> mutborrow : 可變借用
owner --> lifetime : 生命週期標註
box --> heap : 堆積分配
rc --> heap : 引用計數
arc --> heap : 原子引用計數
stack --> owner : 棧上分配
note right of owner
每個值只有一個所有者
所有者離開作用域時值被釋放
end note
@enduml此圖示展示了專案中不同檔案之間的關係。main.rs 是主檔案,它參照了 spores.rs 和 plant_structures/ 目錄下的多個檔案。
程式碼解析
以下是一個範例程式碼,展示瞭如何在 Rust 中使用模組:
// 定義一個名為 my_module 的模組
mod my_module {
pub fn say_hello() {
println!("Hello, world!");
}
}
fn main() {
// 使用 my_module 中的 say_hello 函式
my_module::say_hello();
}
內容解密:
mod my_module { ... }:定義了一個名為my_module的模組。pub fn say_hello() { ... }:在my_module中定義了一個公開的函式say_hello。my_module::say_hello();:在main函式中呼叫了my_module中的say_hello函式。
這個範例展示瞭如何在 Rust 中定義和使用模組,以及如何控制函式的可見性。