Rust 的模式匹配機制簡化了複雜資料結構的處理流程,尤其在搭配參照使用時,更能展現其靈活性。開發者可以利用模式匹配安全地存取和操作資料,同時避免所有權轉移帶來的問題。理解 refref mut& 的區別,以及如何結合模式守衛和 @ 繫結,是撰寫高效 Rust 程式碼的關鍵。此外,Rust 的列舉型別與模式匹配的結合,讓程式碼更加簡潔易懂,並提升了程式碼的安全性。特徵和泛型則提供了多型特性,讓程式碼可以更具彈性地處理不同型別的資料,進一步提升程式碼的可重用性和可維護性。

深入理解 Rust 中的模式匹配(Pattern Matching)與參照

在 Rust 程式設計中,模式匹配是一種強大且靈活的工具,用於解構和檢查資料結構。透過模式匹配,開發者可以更簡潔地處理不同的資料情況,並有效地避免錯誤。本文將探討 Rust 中的模式匹配機制,特別是在處理參照(references)時的行為和最佳實踐。

模式匹配基礎

Rust 的模式匹配允許開發者根據不同的模式來執行不同的程式碼路徑。最常見的模式匹配是在 match 表示式中使用。例如:

match account {
    Account { name, language, .. } => {
        ui.greet(name, language);
    }
}

在這個例子中,Account 結構體被解構,其中的 namelanguage 欄位被提取出來。然而,如果 account 之後還需要被使用,這種做法會導致編譯錯誤,因為 namelanguage 被移動(moved)出了 account

使用 ref 關鍵字借用匹配值

為了避免移動值,可以使用 ref 關鍵字來借用匹配的值:

match account {
    Account { ref name, ref language, .. } => {
        ui.greet(name, language);
        ui.show_settings(&account); // 現在這行是合法的
    }
}

這裡,namelanguage 成為對應欄位的參照,因此 account 沒有被消費,可以繼續使用。

程式碼解析:

  1. ref nameref language:這兩個表示式借用了 account 中對應欄位的參照,避免了值的移動。
  2. ui.show_settings(&account):由於 account 僅被借用而非被消費,因此這行程式碼是合法的。

處理可變參照

如果需要修改匹配的值,可以使用 ref mut 來獲得可變參照:

match line_result {
    Err(ref err) => log_error(err), // err 是 &Error(分享參照)
    Ok(ref mut line) => { // line 是 &mut String(可變參照)
        trim_comments(line); // 直接修改 String
        handle(line);
    }
}

程式碼解析:

  1. Ok(ref mut line):匹配成功結果,並借用一個可變參照到成功值。
  2. trim_comments(line):利用可變參照直接修改 line 所指向的 String

使用 & 模式匹配參照

ref 相反,& 模式用於匹配參照:

match sphere.center() {
    &Point3d { x, y, z } => ...
}

這裡假設 sphere.center() 傳回一個對 Point3d 的參照。使用 & 模式可以解構這個參照並提取其內部的欄位。

程式碼解析:

  1. &Point3d { x, y, z }:這個模式匹配一個對 Point3d 的參照,並將其欄位解構到 xyz 中。
  2. Rust 自動處理參照:在模式匹配中,Rust 自動跟隨指標,這與通常使用 * 運算子的行為相反。

同時匹配多個可能值

Rust 允許使用垂直線(|)來組合多個模式:

let at_end = match chars.peek() {
    Some(&'\r') | Some(&'\n') | None => true,
    _ => false
};

程式碼解析:

  1. Some(&'\r') | Some(&'\n') | None:這個表示式匹配多個不同的模式,只要其中一個匹配就執行對應的程式碼。

範圍模式匹配

Rust 支援使用 ... 進行範圍模式匹配,例如匹配所有 ASCII 數字:

match next_char {
    '0'...'9' => self.read_number(),
    'a'...'z' | 'A'...'Z' => self.read_word(),
    ' ' | '\t' | '\n' => self.skip_whitespace(),
    _ => self.handle_punctuation()
}

程式碼解析:

  1. '0'...'9':這個範圍模式匹配所有 ASCII 數字字元。

模式守衛(Pattern Guards)

可以為模式新增守衛條件,只有當條件滿足時才會匹配成功:

match robot.last_known_location() {
    Some(point) if self.distance_to(point) < 10 => short_distance_strategy(point),
    Some(point) => long_distance_strategy(point),
    None => searching_strategy()
}

程式碼解析:

  1. Some(point) if self.distance_to(point) < 10:這個模式只有在 point 存在且距離小於 10 時才會匹配成功。

@ 模式繫結

使用 @ 可以將整個匹配值繫結到一個變數上:

match self.get_selection() {
    rect @ Shape::Rect(..) => optimized_paint(&rect),
    other_shape => paint_outline(other_shape.get_outline()),
}

程式碼解析:

  1. rect @ Shape::Rect(..):將匹配到的 Shape::Rect 繫結到變數 rect 上。

Rust 中的模式匹配與列舉(Enums and Patterns)

Rust 語言中的模式匹配(Pattern Matching)與列舉(Enums)是強大的工具,能夠幫助開發者處理複雜的資料結構並撰寫出更安全、更高效的程式碼。這些特性源自函式式程式設計語言,已被證明在多種情境下非常有用。

使用模式匹配處理列舉

在 Rust 中,列舉是一種可以包含不同型別值的資料結構。模式匹配允許我們根據列舉值的不同變體(Variants)執行不同的程式碼。

範例:使用 if let 簡化列舉處理

if let RoughTime::InTheFuture(_, _) = user.date_of_birth() {
    user.set_time_traveler(true);
}

這段程式碼檢查 user.date_of_birth() 是否為 RoughTime::InTheFuture 變體,如果是,則將 user 標記為時空旅人。

範例:使用 while let 處理迭代器

while let Some(_) = lines.peek() {
    read_paragraph(&mut lines);
}

這段程式碼不斷檢查 lines.peek() 是否傳回 Some 值,如果是,則讀取一段文字,直到 lines 被完全處理完畢。

實作二元搜尋樹(Binary Search Tree)

以下是一個使用列舉和模式匹配實作二元搜尋樹的例子:

enum BinaryTree<T> {
    Empty,
    NonEmpty(Box<TreeNode<T>>)
}

struct TreeNode<T> {
    element: T,
    left: BinaryTree<T>,
    right: BinaryTree<T>
}

impl<T: Ord> BinaryTree<T> {
    fn add(&mut self, value: T) {
        match *self {
            BinaryTree::Empty => 
                *self = BinaryTree::NonEmpty(Box::new(TreeNode {
                    element: value,
                    left: BinaryTree::Empty,
                    right: BinaryTree::Empty
                })),
            BinaryTree::NonEmpty(ref mut node) => 
                if value <= node.element {
                    node.left.add(value);
                } else {
                    node.right.add(value);
                }
        }
    }
}

程式碼解析:

  1. 定義二元樹列舉BinaryTree 列舉有兩個變體,EmptyNonEmpty,後者包含一個 TreeNode
  2. add 方法實作:使用模式匹配檢查樹是否為空。如果為空,則建立一個新的節點;如果非空,則遞迴地將新值加入左子樹或右子樹。

Rust 列舉的優勢

Rust 的列舉提供了多項優勢,包括:

  • 記憶體安全:透過嚴格的模式匹配和編譯時檢查,Rust 能夠避免諸如 null 指標解參照等常見錯誤。
  • 高效性:列舉的設計允許編譯器進行最佳化,使得根據列舉的程式碼執行效率更高。
  • 簡潔性:使用列舉和模式匹配可以簡化程式碼,避免冗長的條件判斷和型別檢查。

特徵(Traits)與泛型(Generics)

在程式設計的世界中,有一個重要的發現,那就是能夠撰寫出可以操作多種不同型別值的程式碼,甚至是那些尚未被創造出來的型別。以下是兩個例子:

  • Vec<T> 是泛型的:你可以建立任何型別值的向量,包括那些 Vec 作者從未預料到的、在你的程式中定義的型別。
  • 許多事物都有 .write() 方法,包括 FilesTcpStreams。你的程式碼可以透過參考取得一個寫入器(writer),任何寫入器,並將資料傳送給它。你的程式碼不需要關心寫入器的型別。稍後,如果有人新增了一種新的寫入器型別,你的程式碼將自動支援它。

當然,這種能力在 Rust 中並不是新鮮事。它被稱為多型(polymorphism),是 1970 年代熱門的程式語言技術。如今,它幾乎是普遍的。Rust 支援具有兩個相關特性的多型:特徵(traits)和泛型(generics)。這些概念對許多程式設計師來說都很熟悉,但 Rust 採用了受 Haskell 型別類別(typeclasses)啟發的新穎方法。

特徵(Traits)

特徵是 Rust 對介面或抽象基底類別的實作。一開始,它們看起來就像 Java 或 C# 中的介面。用於寫入位元組的特徵被稱為 std::io::Write,其在標準函式庫中的定義如下:

trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;
    fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
    ...
}

這個特徵提供了幾個方法;我們只展示了前三個。

標準型別 FileTcpStream 都實作了 std::io::WriteVec<u8> 也實作了它。所有這三種型別都提供了名為 .write().flush() 等的方法。

使用不關心其型別的寫入器的程式碼如下所示:

use std::io::Write;

fn say_hello(out: &mut dyn Write) -> std::io::Result<()> {
    out.write_all(b"hello world\n")?;
    out.flush()
}

out 的型別是 &mut dyn Write,表示「一個可變的參考,指向任何實作 Write 特徵的值」。

use std::fs::File;

let mut local_file = File::create("hello.txt")?;
say_hello(&mut local_file)?; // 可行
let mut bytes = vec![];
say_hello(&mut bytes)?; // 也可行
assert_eq!(bytes, b"hello world\n");

本章首先展示如何使用特徵、它們的工作原理以及如何定義自己的特徵。但特徵的功能遠不止於此。我們將使用它們為現有的型別(甚至是像 strbool 這樣的內建型別)新增擴充方法。我們將解釋為什麼為型別新增特徵不會佔用額外的記憶體,以及如何使用特徵而無需虛擬方法呼叫的開銷。我們將看到內建特徵是 Rust 提供的用於運算元多載(operator overloading)和其他功能的語言鉤子(hook)。我們還將介紹 Self 型別、關聯方法(associated methods)和關聯型別(associated types),這三個功能是 Rust 從 Haskell 中借鑒而來的,能夠優雅地解決其他語言需要透過變通方法和 hack 來解決的問題。

泛型(Generics)

泛型是 Rust 中的另一種多型形式。與 C++ 範本類別似,泛型函式或型別可以用於多種不同型別的值。

/// 給定兩個值,選出較小的一個。
fn min<T: Ord>(value1: T, value2: T) -> T {
    if value1 <= value2 {
        value1
    } else {
        value2
    }
}

這個函式中的 <T: Ord> 表示 min 可以用於任何實作 Ord 特徵的型別 T 的引數——即任何有序型別。編譯器會為你實際使用的每個型別 T 生成自訂的機器碼。

內容解密:

此段程式碼定義了一個泛型函式 min,它接受兩個相同型別 T 的引數,並傳回其中較小的一個。T 必須實作 Ord 特徵,這意味著 T 必須支援比較運算。編譯器會根據實際使用的型別 T 生成對應的機器碼,以確保效能和型別安全。

泛型和特徵密切相關。Rust 要求我們在事先宣告 T: Ord 需求(稱為約束),然後再使用 <= 運算元比較兩個 T 型別的值。因此,我們還將討論 &mut dyn Write<T: Write> 之間的相似之處、差異以及如何選擇使用特徵的這兩種方式。

使用特徵

特徵是一種特性,任何給定的型別都可以支援或不支援它。大多數情況下,特徵代表了一種能力:某個型別可以做某件事情。

  • 一個實作了 std::io::Write 的值可以寫出位元組。
  • 一個實作了 std::iter::Iterator 的值可以產生一序列的值。
  • 一個實作了 std::clone::Clone 的值可以在記憶體中建立自己的複製品。
  • 一個實作了 std::fmt::Debug 的值可以使用 println!(){?:} 格式規範器進行列印。

這些特徵都是 Rust 標準函式庫的一部分,許多標準型別都實作了它們。

  • std::fs::File 實作了 Write 特徵;它將位元組寫入本機檔案。std::net::TcpStream 將位元組寫入網路連線。Vec<u8> 也實作了 Write;對位元組向量的每個 .write() 呼叫都會將一些資料附加到向量的末尾。
  • Range<i32>(例如 0..10 的型別)實作了 Iterator 特徵,其他與切片、雜湊表等相關的迭代器型別也是如此。
  • 大多數標準函式庫型別都實作了 Clone。主要的例外是像 TcpStream 這樣代表的不僅僅是記憶體中的資料的型別。
  • 同樣,大多數標準函式庫型別都支援 Debug

關於特徵方法,有一個不尋常的規則:特徵本身必須在作用域中。否則,它的所有方法都會被隱藏。

let mut buf: Vec<u8> = vec![];
buf.write_all(b"hello")?; // 錯誤:沒有名為 `write_all` 的方法

內容解密:

此段程式碼嘗試呼叫 write_all 方法,但由於沒有將 Write 特徵引入作用域,因此編譯器無法找到該方法。解決方法是新增 use std::io::Write; 陳述,將 Write 特徵引入作用域,使其方法可供使用。

使用特徵(Traits)進行多型程式設計

在Rust中,特徵(Traits)是一種定義分享行為的方式,讓不同的型別可以實作相同的介面。本章將介紹如何使用特徵來撰寫多型程式碼,主要涵蓋特徵物件(Trait Objects)和泛型(Generics)兩種方法。

為什麼需要引入特徵?

在前面的章節中,我們已經看到了std::io::Write特徵的例子。Write特徵定義了一組方法,例如write_allflush,這些方法可以被多種型別實作,例如Vec<u8>File。當我們想要撰寫可以處理多種型別的程式碼時,特徵就變得非常有用。

特徵物件(Trait Objects)

Rust不允許直接使用特徵型別作為變數型別,因為編譯器需要在編譯時期知道變數的大小。例如,下面的程式碼無法編譯:

use std::io::Write;
let mut buf: Vec<u8> = vec![];
let writer: Write = buf; // 錯誤:`Write` 沒有固定的大小

要解決這個問題,我們可以使用特徵物件,也就是對某個特徵的參照。下面的程式碼展示瞭如何使用特徵物件:

let mut buf: Vec<u8> = vec![];
let writer: &mut dyn Write = &mut buf; // 正確

在記憶體中,特徵物件是一個胖指標(Fat Pointer),它包含兩個部分:指向實際值的指標和指向該值型別資訊表的指標。這使得Rust可以在執行時期根據實際型別呼叫正確的方法。

特徵物件的記憶體佈局

下圖展示了特徵物件在記憶體中的佈局:

  +
---
-
---
-
---
-
---
+
  |  指標         |
  |  (指向實際值)  |
  +
---
-
---
-
---
-
---
+
  |  vtable 指標  |
  |  (指向型別資訊) |
  +
---
-
---
-
---
-
---
+

這種佈局類別似於C++中的虛表(vtable)。Rust在編譯時期生成vtable,並在執行時期使用它來呼叫正確的方法。

泛型函式(Generic Functions)

除了使用特徵物件外,Rust還提供了泛型函式來實作多型。泛型函式是一種可以處理多種型別的函式。下面是一個例子:

fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> {
    out.write_all(b"hello world\n")?;
    out.flush()
}

在這個例子中,W是一個型別引數,它代表某個實作了Write特徵的型別。泛型函式可以提供更好的效能,因為編譯器可以在編譯時期根據實際型別生成特定的程式碼。

#### 內容解密:

  • fn say_hello<W: Write>(out: &mut W) -> std::io::Result<()> { ... }:這是一個泛型函式的定義,W是型別引數,必須實作Write特徵。
  • out.write_all(b"hello world\n")?;:呼叫write_all方法將字串寫入輸出。
  • out.flush():呼叫flush方法確保資料被寫入。
  • -> std::io::Result<()>:函式傳回一個結果型別,表示操作是否成功。

泛型函式和特徵物件都可以實作多型,但它們有不同的使用場景和效能特點。選擇合適的方法取決於具體的需求和效能要求。