作者註: 這是一篇在 2019 年開了頭卻未能完成的草稿,於 2026 年藉由 AI 協作補齊內容,並保留了 2019 年當時的技術生態與時空視角。

在上一篇 Learning Rust 中,我分享了自己接觸 Rust 的背景、為了看懂 Fuchsia OS 系統服務而重拾這門語言的轉折點,以及幾本適合入門打底的書籍。

上一篇主要是全面性的入門材料。在跨過基礎語法、所有權(ownership)與借用(borrowing)的第一道門檻後,進入中進階階段時,往往會面臨更多細節設計的挑戰。這篇接續先前的承諾,整理我覺得極具啟發性的進階資源、精選文章、初學者容易卡關的語義盲點(例如 Option::take() 的不可或缺),以及身為長期 Go 語言愛好者,在 2019 年如何看待 Go 與 Rust 的定位抉擇。

進階書籍:深入 Unsafe 的底層規則

當你寫了一陣子 safe Rust,想要進一步探究效能極致、自訂底層資料結構,或是與 C 語言進行 FFI(Foreign Function Interface)介接時,就必須推開 unsafe 的大門:

很多人剛學 Rust 時會有一種誤解,以為寫 Rust 就應該完全杜絕 unsafe。但在現實世界中,如果沒有 unsafe,像 Vec、LinkedList 這些標準函式庫的核心容器便無法實現。

《The Rustonomicon》就是官方專門為這群想要了解底層機制的開發者撰寫的進階指南。它深入探討了記憶體佈局(memory layout)、未定義行為(Undefined Behavior, UB)、aliasing 規則、指標轉型以及 panic 安全(panic safety)與 RAII 銷毀保證。

即使你在日常開發中只寫 safe Rust,我也非常推薦抽空讀讀這本書。當你理解了編譯器背後在防範什麼底層未定義行為時,你就能從機制層面理解其設計哲學,不再把生命週期(lifetimes)與 borrow checker 的嚴格檢查視為單純的束縛。

精選技術文章

網路上有很多優秀的工程師針對 Rust 的特定主題寫了深入淺出的文章,以下幾篇是我在學習過程中覺得含金量極高的技術分享:

閉包的本質:Finding Closure in Rust

Rust 的閉包(closures)依捕獲環境的方式與呼叫約束,實作了 FnOnce、FnMut 與 Fn 三種 trait(其繼承層級為 FnOnce ⊃ FnMut ⊃ Fn)。初學者常常會被這三個 trait 搞得暈頭轉向。

Huon Wilson 這篇文章以非常清晰的步驟,剖析編譯器如何將閉包「解糖(desugar)」為一個匿名的 struct 以及對應的 trait 實作。文章深入解釋了閉包環境捕獲(by reference 實作 Fn、by mutable reference 實作 FnMut、by value 實作 FnOnce)與呼叫方式之間的關聯,讀完後會讓人真正體會到 Rust 所謂的「零成本抽象(zero-cost abstractions)」是多麼優雅地落實在型別系統之中。此外,在跨執行緒傳遞閉包時,編譯器自動推導的 Send + 'static 邊界更是並發與非同步程式設計的核心基石。

Option 的優雅模式:Rust: Using Options by example

在剛從其他語言轉來 Rust 時,許多人處理 Option<T> 的方式依然充滿傳統指令式語言的影子,滿滿的 if let Some(x) = ... 甚至動不動就 .unwrap()。

Ameya Lokare 這篇文章透過豐富且簡潔的程式碼範例,展示了如何活用 map、and_then、unwrap_or_else、filter 與 take 等豐富的組合子(combinators)。學會用函數式風格來操作 Option,是從「能寫出跑得動的 Rust」跨入「寫出 idiomatic 的 Rust」的重要轉折點。

非同步的基石:Futures 與 Tokio

  • Futures · Tokio(註:官方文件現已更新為現代 async/await 教學;而在 2019 年當時,非同步程式設計主要奠基於 futures 0.1 的組合子模型)

在 2019 年的這個時間點,Rust 2018 edition 已經釋出,而大家引頸期盼的 async/await 語法還在 nightly 版本中快速迭代(預計今年稍晚才會正式進入穩定版)。此時想要處理高併發網路 I/O,主流依然是透過 Tokio 與 futures 0.1 的組合子。

理解 Rust 的 poll-based future 模型非常關鍵:與 JavaScript 或 Go 不同,Rust 中的 Future 是被動惰性的(lazy),建立 Future 物件並不會在背景啟動執行緒,它必須被主動丟進 executor 透過 poll 方法驅動。這篇官方文件是掌握非同步本質必讀的敲門磚。

批判性回顧:Julio Merino 的 Rust Review 系列

前 NetBSD 開發者、當時任職於 Google 的 Julio Merino 撰寫了一系列針對 Rust 的全面評測。

這系列文章的可貴之處在於他不盲目推崇,而是從資深系統軟體工程師的角度,既盛讚 Rust 在型別安全與防範並行錯誤上的突破,也客觀剖析了學習曲線陡峭、編譯速度緩慢、以及依賴套件(Cargo crates)膨脹所帶來的維護考驗。讀一讀這類冷靜客觀的評論,有助於我們在架構選型時保持清醒。

語言內部的特殊待遇:Rust Tidbits: Box Is Special

Manish Goregaokar 的這篇小品揭露了 Box<T> 在編譯器眼中的特權。在標準的自訂智慧指標(如 Rc 或 Arc)中,因為 Deref trait 的簽名是 fn deref(&self) -> &Self::Target,你無法在解引用時把內部的值所有權移出(move out of deref)。

但是對於 Box<T>,編譯器特別給予了多重特例支援:除了允許直接寫 *b 取得內部值的所有權之外,還享有內建解構器、支援 Box<Self> 方法接收者(method receiver),以及不受孤兒規則限制(可在定義 T 的 crate 中為 Box<T> 實作外部 trait)。這篇文章用簡潔有趣的語調揭開了編譯器特權底下的秘密。

物件與設計模式:Builder Pattern、Trait Objects、Box 與 Rc

習慣了物件導向(OOP)繼承體系的工程師在初學 Rust 時,常常苦惱於不知道該如何組織程式碼結構。Alexandre Beslic(@abronan)這篇文章串聯了 Builder 模式、Trait Objects 的動態分派(Box<dyn Trait>),以及透過引用計數指標(Rc<T>)處理多重所有權的時機,對於建立良好的程式碼設計心智模型非常有幫助。

常見困惑與盲點解析

在學習 Rust 的過程中,有幾個看似微小卻極容易困惑的點,特別值得拿出來探討:

&foo 與 foo.as_ref() 的差異

剛學 Rust 時,常常會看到有人寫 &foo,有人卻寫 foo.as_ref(),這兩者有何本質差別?

簡而言之:

  • &foo 是編譯器原生的取參照操作。若 foo 型別為 Foo,在一般運算式中 &foo 產生 &Foo(但在模式比對遇到 &mut foo 時,綁定模式亦可能推導出 &mut Foo)。
  • foo.as_ref() 是呼叫 AsRef<T> trait 定義的方法:fn as_ref(&self) -> &T。它能返回的目標型別不限於 &Foo,而是可以取得任何實作了 AsRef<T> 的目標型別參照。

最經典的例子就是 String 與 &str:

let s = String::from("hello");

// ⚠️ 若不給予型別註記,直接呼叫會觸發編譯錯誤 error[E0283]:
// let r = s.as_ref();
// 原因在於 String 同時實作了 AsRef<str>、AsRef<[u8]>、AsRef<Path> 與 AsRef<OsStr>,
// 編譯器在缺乏目標型別線索時無法推導。

let r1: &String = &s;           // 原生借用,型別為 &String
let r2: &str    = s.as_ref();   // 明確指定目標型別,直接取得 &str

AsRef 本質上是在不消耗原始值所有權的前提下提供低成本的唯讀視圖(與消耗所有權做型別轉換的 Into 截然不同)。在撰寫通用函式 API 時,AsRef 展現了相當關鍵的靈活性:

fn print_length<T: AsRef<str>>(input: T) {
    println!("長度: {}", input.as_ref().len());
}

// 呼叫者可以直接傳入 &str 或 String,完全不需要手動做格式轉換
print_length("world");
print_length(String::from("world"));

另一個常見的應用是檔案路徑:AsRef<Path> 讓你的函式可以同時無縫接受 &str、String、&Path 或 PathBuf,搭配 Borrow<T> 與 ToOwned 構成 Rust 兼顧效能與人體工學的型別抽象體系。

到底在什麼情境下需要使用 Option::take()?

這是我在初學 Rust 時經常遇到的另一個痛點。

我們常會遇到這樣的情況:你擁有一個結構體的可變借用(&mut self),裡面有一個欄位是 Option<T>。在某個方法中,你想要取得裡面那個 T 的所有權,拿去傳給某個需要 ownership 的函式:

struct Worker {
    state: Option<State>,
}

impl Worker {
    fn transition(&mut self) {
        // 編譯報錯:cannot move out of `self.state` which is behind a mutable reference (error[E0507])
        // let old_state = self.state.unwrap();
    }
}

編譯器之所以嚴厲拒絕,是因為從 &mut self 參照中抽走資料會讓該記憶體位置處於未初始化(uninitialized)狀態,借用檢查器無法跨參照保證其有效性;此外若結構體實作了 Drop trait,任何欄位的部分移動更會直接觸發 error[E0509]。

這時 Option::take() 就是救星:

impl Worker {
    fn transition(&mut self) {
        // take() 會將值取出並回傳 Some(old_state),同時在原位置留下 None
        if let Some(old_state) = self.state.take() {
            let new_state = process(old_state);
            self.state = Some(new_state);
        }
    }
}

take() 的妙處在於它是單一的取值操作(底層為 mem::replace(self, None)):把值抽走的同時,順手塞入一個合法的 None。這樣既拿到了所有權,結構體內部在任何時刻也依然處於已初始化的合法狀態。

另一個經典場景是自訂鏈結串列(Linked List)或樹狀節點的析構。Rust 預設的遞迴 Drop 在遇到深層巢狀鏈結結構時,會因為呼叫堆疊過深而引發 stack overflow。透過在 Drop 實作中搭配 take() 進行迭代式釋放(iterative drop),能平坦化呼叫鏈:

struct List {
    head: i32,
    next: Option<Box<List>>,
}

impl Drop for List {
    fn drop(&mut self) {
        let mut cursor = self.next.take();
        while let Some(mut node) = cursor {
            // 每次迴圈切斷下一節點並接管其所有權,使前一節點在局部作用域平坦釋放
            cursor = node.next.take();
        }
    }
}

在日常開發中,相關的同族 API 還包括:mem::take(&mut val)(要求目標型別實作 Default)、mem::replace(&mut place, new_val) 以及 Option::insert(val) 等實用工具。

Go vs Rust:2019 年的雙城思維

身為 Go 語言的長期使用者,在深入學習 Rust 之後,無可避免地會面臨兩者的比較:

社群上常有人喜歡挑起語言之間的優劣爭論,但在我看來,這兩個語言在設計哲學上根本不是零和博弈,而是為了完全不同的目標進行極致優化:

解決問題的維度不同

  • Go 追求的是開發者的心智簡潔與團隊一致性: Go 的語法極其精簡,刻意捨棄了許多複雜的高階抽象特性。搭配高速編譯、強大的內建工具鏈(gofmt、go test)、Goroutine 並發模型以及高效能垃圾回收(GC)。它讓任何水平的工程師都能在短時間內寫出風格一致、易於維護的後端微服務與網路工具。
  • Rust 追求的是極致的控制力與記憶體絕對安全: Rust 沒有 GC,執行時期負擔僅限於 allocator 與 panic 處理機制;在 no_std 裸機或嵌入式環境下更可做到近乎零執行時期負擔。它把所有驗證壓力前置到編譯期,透過型別系統與借用檢查徹底杜絕資料競爭(data races)與記憶體安全隱患。它的型別表現力極為強大,但也伴隨著顯著的心智負擔與編譯成本。

如何選擇?

Matthias Endler 與 Julio Merino 的文章給出了非常中肯的指引:

  1. 選擇 Go 的情境:如果你要做的是一般的網路 API、微服務、內部 CLI 工具,或是需要帶領背景多元的團隊快速交付業務價值,Go 通常是更務實、性價比更高的選擇。
  2. 選擇 Rust 的情境:如果你在開發底層系統軟體(如 Fuchsia OS 的底層服務)、作業系統核心、資料庫引擎、瀏覽器渲染器、嵌入式系統,或是音訊與影像即時編解碼,任何 GC 停頓都會造成致命影響,且對記憶體 footprint 有極嚴苛要求的場景,Rust 是當前無可取代的現代化利器。

結語

從 Go 跨進 Rust 的過程,就像是習慣了自排車的便利後,重新學習開手排車——你必須自己思考離合器接合點、檔位與轉速比,一開始難免挫折手忙腳亂;但一旦開順了,那種每一滴引擎動力與機械運轉都在自己精確掌控之下的充實感,是前所未有的。

學習 Rust 不只是多掌握了一門語法,更是一趟重新反思記憶體管理、並發安全與型別抽象的思維重塑之旅。

參考資源清單