Rust 的 Deref 特性允許我們透過 * 運算元解參照值,編譯器也利用它在智慧型指標上呼叫方法。雖然 Deref 對於簡化包裝結構體的操作很有用,但它並非真正的多型。使用 Deref 模擬多型並非 Rust 的慣用法,反而可能造成程式碼混淆,降低可讀性。更推薦的做法是使用特性和泛型,或者直接提供方法傳回內部值。例如,比起使用 Deref 使 Person 結構體表現得像 String 來取得名字,不如直接提供 first_name()first_name_len() 方法。特性物件是 Rust 中實作多型的重要機制,透過定義和實作特性,可以讓不同型別的物件分享相同的介面。以 Animal 特性為例,DogCat 都可以實作 speak()name() 方法,並透過特性物件向量實作多型行為。儘管使用 Deref 可以達到類別似效果,但這種方式並不直觀,容易造成誤解。

10.6 使用 Deref 模擬多型

多型是一種技術,允許我們將不同型別的物件視為同一型別。物件導向語言鼓勵透過子型別或繼承來使用多型,而這些在 Rust 中並不存在。

有時,我們使用 Deref 特性來使物件的操作更加方便,讓編譯器透過 Deref 強制轉換來推斷我們想要呼叫的方法。在某種程度上,我們有效地模擬了其他語言(如 C++ 和 Java)中可能見到的多型。這種方法不一定是壞的,但它可能表明我們沒有以 Rust 的慣用方式思考我們的設計。

Deref 特性(及其可變對應特性 DerefMut)允許我們透過使用 * 運算元來解參照一個值,就像 *value 一樣。此外,編譯器隱式地使用 Deref 特性來允許在被智慧指標(如 BoxRcArc)包裹的值上呼叫方法。換句話說,如果我們有 let value: Box<T> = Box::new(T);,我們可以在 value 上呼叫方法,就像它是 T 一樣,而無需解參照它,就像 value.method()

第 7 章討論了包裝結構體,並展示了使用 Deref 特性如何使包裝結構體表現得像它所包裝的型別一樣。這種情況是 Deref 的常見用法,類別似於 Rust 的智慧指標,它們也以這種方式使用 Deref,但它不是多型。在許多情況下,您可以透過使用特性和泛型或簡單地提供一個傳回內部值的方法來避免使用 Deref 模擬多型。下面的清單說明瞭在一個簡單的例子中使用 Deref,該例子使用 Deref 強制轉換來傳回元組結構體 Person 的第一個成員。

use std::ops::Deref;

struct Person(String, String, u32);

impl Deref for Person {
    type Target = String;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let ferris = Person("Ferris".to_string(), "Bueller".to_string(), 17);
    println!("Hello, {}!", *ferris);
    println!("The length of a person is {}", ferris.len());
}

內容解密:

  • 在這個例子中,我們定義了一個元組結構體 Person,它包裝了兩個字串(名字和姓氏)和一個年齡。
  • 我們為 Person 實作了 Deref 特性,以便能夠解參照到 String
  • main 函式中,我們建立了一個 Person 例項,並透過 Deref 強制轉換呼叫了 len() 方法,就像它是一個 String 一樣。

為什麼不該這樣做

在這個例子中,我們傳回了人的名字,但讀者不清楚為什麼要這樣做。為什麼不直接從一個方法傳回名字?檢視這段程式碼的人會感到困惑,因為它不是 Rust 的慣用法。我們正在使 Person 表現得像一個 String 用於特定的使用案例,但為什麼要這樣做並不清楚。當我們執行上述程式碼時,它產生了以下輸出:

Hello, Ferris!
The length of a person is 6

我們可以輕易地實作一個 first_name() 方法來傳回內部的 String,或者提供一個 first_name_len() 方法,這將更加清晰(儘管如果我們傳回字串,那麼就足以透過 ferris.first_name().len() 取得長度)。存取名字的微小便利並不值得引入由 Deref 帶來的模糊性。如果我們想要提供一個 first_name_len() 方法,我們可以如下實作:

impl Person {
    fn first_name_len(&self) -> usize {
        self.0.len()
    }
}

內容解密:

  • 這個實作直接在 Person 上提供了一個方法來取得名字的長度,而不是依賴 Deref 強制轉換。
  • 這種方法更加清晰和直接,避免了使用 Deref 可能帶來的混淆。

使用特性物件實作多型

讓我們來看一個例子,它展示瞭如何在 Rust 中實作多型。這個例子展示了一個實作了 Animal 特性的 Dog 和一個也實作了 Animal 特性的 Cat

trait Animal {
    fn speak(&self) -> &str;
    fn name(&self) -> &str;
}

struct Dog {
    name: String,
}

impl Dog {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }
}

impl Animal for Dog {
    fn speak(&self) -> &str {
        "Woof!"
    }

    fn name(&self) -> &str {
        &self.name
    }
}

struct Cat {
    name: String,
}

impl Cat {
    fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
        }
    }
}

impl Animal for Cat {
    fn speak(&self) -> &str {
        "Meow!"
    }

    fn name(&self) -> &str {
        &self.name
    }
}

fn main() {
    let dog = Box::new(Dog::new("Rusty"));
    let cat = Box::new(Cat::new("Misty"));
    let animals: Vec<Box<dyn Animal>> = vec![dog, cat];
    for animal in animals {
        println!("{} says {}", animal.name(), animal.speak());
    }
}

內容解密:

  • 這個例子使用了特性物件來建立一個會叫的動物的向量。
  • 我們定義了一個 Animal 特性,並為 DogCat 實作了它。
  • main 函式中,我們建立了 DogCat 的例項,並將它們放入一個向量中,然後遍歷向量並呼叫每個動物的 speak() 方法。

使用 Deref 模擬多型

現在,讓我們建立一些類別似的東西,但這次使用 Deref 來模擬多型。我們將建立一個具有 name 屬性的 Animal 結構體,並透過傳回內部的 Animal 使用 Deref 將其視為 DogCat 結構體的超類別。

use std::ops::Deref;

struct Animal {
    name: String,
}

impl Animal {
    fn new(name: &str) -> Animal {
        Animal { name: name.to_string() }
    }

    fn name(&self) -> &str {
        &self.name
    }
}

struct Dog(Animal);

impl Dog {
    fn new(name: &str) -> Self {
        Self(Animal::new(name))
    }

    fn speak(&self) -> &str {
        "Woof!"
    }
}

impl Deref for Dog {
    type Target = Animal;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct Cat(Animal);

impl Cat {
    fn new(name: &str) -> Self {
        Self(Animal::new(name))
    }

    fn speak(&self) -> &str {
        "Meow!"
    }
}

impl Deref for Cat {
    type Target = Animal;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let dog = Dog::new("Rusty");
    let cat = Cat::new("Misty");
    println!("{} says {}", dog.name(), dog.speak());
    println!("{} says {}", cat.name(), cat.speak());
}

內容解密:

  • 在這個例子中,我們使用 DerefDogCat 視為具有共同超類別 Animal
  • 這種方法可以模擬多型,但不是 Rust 的慣用法,可能會導致混淆。

總的來說,雖然可以使用 Deref 來模擬多型,但更慣用的 Rust 方法是使用特性和泛型。這樣可以使程式碼更清晰、更易於理解。

過度使用智慧型指標的陷阱

在 Rust 程式語言中,智慧型指標是一種非常有用的工具,它們提供了自動化的記憶體管理功能,使得開發者能夠更輕鬆地處理複雜的資料結構和記憶體分配問題。然而,過度使用或不當使用智慧型指標可能會導致程式碼變得複雜、效率低下,甚至引入難以察覺的錯誤。

智慧型指標的型別與用途

Rust 提供了多種智慧型指標,包括 BoxRcArcRefCellCell 等,每種指標都有其特定的用途和適用場景。

  • Box:提供堆積疊分配和釋放,允許在編譯時期大小未知的情況下,將值存放在具有固定大小的物件中。
  • Rc:允許分享所有權,提供多個所有者對同一值的參照計數功能。
  • Arc:提供執行緒安全的參照計數,允許跨執行緒分享所有權。
  • RefCellCell:提供內部可變性,允許在不破壞 Rust 的所有權規則下,修改被分享的值。

程式碼示例:智慧型指標的基本使用

use std::rc::Rc;
use std::sync::{Arc, Mutex};

fn main() {
    // 使用 Box 進行堆積疊分配
    let b = Box::new(5);
    println!("b = {}", b);

    // 使用 Rc 進行分享所有權
    let rc = Rc::new(5);
    let rc_clone = Rc::clone(&rc);
    println!("rc = {}, rc_clone = {}", rc, rc_clone);

    // 使用 Arc 進行跨執行緒分享所有權
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = std::thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

內容解密:

  1. 在這個例子中,我們展示了三種不同的智慧型指標的使用方法:BoxRcArc
  2. Box 用於在堆積疊上分配一個值,並將其存放在一個具有固定大小的 Box 中。
  3. Rc 用於實作分享所有權,允許多個變數指向同一個值。
  4. Arc 結合 Mutex 用於跨執行緒分享一個可變的值,並確保執行緒安全。

過度使用智慧型指標的問題

雖然智慧型指標非常有用,但過度使用或不當使用可能會導致以下問題:

  • 冗餘的記憶體分配:例如,在 Vec 中存放 Box,會導致兩次記憶體分配,一次是 Vec 的分配,另一次是 Box 的分配。
  • 效能下降:過多的智慧型指標可能會導致效能下降,因為每次存取都需要進行額外的運算,如參照計數的更新。
  • 程式碼複雜度增加:過度使用智慧型指標可能會使程式碼變得更加複雜,難以理解和維護。

程式碼示例:過度使用智慧型指標

fn main() {
    // 過度使用 Box 和 Vec
    let mut string_box_vec: Vec<Box<String>> =
        vec![Box::new(String::from("unnecessarily boxed string"))];
    let mut string_vec: Vec<String> =
        vec![String::from("this is okay")];

    let boxed_string = string_box_vec.remove(0);
    let normal_string = string_vec.remove(0);

    println!("boxed_string = {}", boxed_string);
    println!("normal_string = {}", normal_string);
}

內容解密:

  1. 在這個例子中,我們展示了過度使用智慧型指標的情況:將 String 存放在 Box 中,然後再將 Box 存放在 Vec 中。
  2. 這種做法是冗餘的,因為 Vec 本身已經在堆積疊上分配記憶體,而 Box 又進行了一次額外的分配。
  3. 正確的做法是直接將 String 存放在 Vec 中,避免額外的記憶體分配。

最佳實踐

為了避免過度使用智慧型指標,開發者應該遵循以下最佳實踐:

  • 謹慎選擇合適的智慧型指標:根據實際需求選擇合適的智慧型指標,避免不必要的複雜度。
  • 避免冗餘的記憶體分配:檢查程式碼中是否存在冗餘的記憶體分配,如在 Vec 中存放 Box
  • 最佳化程式碼結構:透過最佳化程式碼結構和邏輯,減少對智慧型指標的依賴。

智慧型指標選擇流程

  graph LR
    A[開始] --> B{是否需要堆積疊分配?}
    B -->|是| C{是否需要分享所有權?}
    B -->|否| D[使用普通變數]
    C -->|是| E{是否需要跨執行緒分享?}
    C -->|否| F[使用 Rc]
    E -->|是| G[使用 Arc]
    E -->|否| F
    G --> H[結合 Mutex 或 RwLock 使用]

圖表翻譯:

此圖表展示了選擇合適的智慧型指標的流程。首先,根據是否需要堆積疊分配進行判斷。如果不需要,則直接使用普通變數。如果需要堆積疊分配,則進一步判斷是否需要分享所有權。如果不需要分享所有權,則視情況使用 Box。如果需要分享所有權,則判斷是否需要跨執行緒分享。如果需要跨執行緒分享,則使用 Arc,並結合 MutexRwLock 以確保執行緒安全。如果不需要跨執行緒分享,則使用 Rc。透過這個流程,可以根據具體需求選擇最合適的智慧型指標。

Rust 程式設計:反模式與最佳實踐

在 Rust 程式設計的世界中,瞭解並避免反模式是提升程式碼品質和效能的關鍵。本章節將探討 Rust 中的常見反模式,並提供實用的替代方案和最佳實踐。

瞭解反模式

反模式(Antipatterns)是指在特定情境下或所有情況下被認為有害的程式設計實踐。雖然反模式的使用往往是主觀的,但在某些情況下,它們客觀上是糟糕的,例如當它們不安全、低效或難以維護時。

常見的 Rust 反模式

  1. 濫用 unsafe 關鍵字
    unsafe 是 Rust 中必要的部分,但濫用或過度使用它可能會導致問題。雖然幾乎不可能完全避免使用 unsafe 程式碼(至少是間接使用),但在遇到它時應該仔細審查。絕對不要使用 unsafe 來繞過借用檢查器。

  2. 使用 unwrap() 方法
    unwrap() 是 Rust 中的常見反模式,經常在處理 OptionResult 值時偷懶使用。透過使用 expect()map()and_then()unwrap_or()? 運算元等方法,可以相對容易地避免使用 unwrap()

  3. 不必要的複製
    clone() 方法有時被濫用,經常在不需要時使用。雖然它並非總是壞事,但可能導致效能問題和記憶體膨脹,是一種程式碼異味。

  4. 濫用 Deref 特徵
    有時,Deref 特徵被用來模擬多型,這在 Rust 中可能會造成混淆。相反,應該依賴特徵或泛型,或者簡單地提供一個傳回所需內部值的方法。

  5. 全域資料和單例模式
    全域資料和單例模式在程式設計中經常被視為反模式。它們可能導致各種問題,例如緊密耦合、測試性差和難以推斷程式碼。在 Rust 中,可以使用諸如 lazy_static 之類別的 crate 來建立全域資料或單例,但在這樣做之前要三思。

最佳實踐

  • 善用 Vec:對於許多工作負載來說,Vec 很快,經常是最佳選擇。在各種基準測試中,它通常比 HashSetHashMapBTreeSetBTreeMapLinkedList 更快,而且記憶體效率更高。
  • 審慎使用智慧指標:智慧指標非常有用,但有可能被濫用或使用錯誤的智慧指標。如果使用智慧指標作為繞過借用檢查器的逃生艙,請重新考慮您的設計。

安裝 Rust

為了充分利用本文,您需要安裝一個可用的 Rust 工具鏈。如果您以前從未使用過 Rust,您需要安裝最新版本的 Rust 工具鏈,其中包括編譯器和標準函式庫。根據您的作業系統,您可能還需要安裝一些開發工具,以編譯和執行本文附帶的所有程式碼範例。

在 macOS 上安裝工具

$ brew install git
$ sudo xcode-select --install

在 Linux 系統上安裝工具

對於根據 Debian 的系統,請使用以下命令:

$ apt-get install git build-essential

對於根據 Red Hat 的系統,請使用以下命令:

$ yum install git make automake gcc gcc-c++

在 Windows 上安裝工具

如果您使用的是根據 Windows 的作業系統,您需要從 https://rustup.rs 下載最新版本的 rustup。您也可以使用 Windows Subsystem for Linux (WSL) 並按照前一節中的 Linux 安裝說明進行操作。

使用 rustup 管理 Rust 元件

安裝 rustup 後,您需要安裝 Rust 編譯器和相關工具。至少,我建議您安裝 Rust 的穩定版和夜間版工具鏈。

安裝 rustc 和其他元件

預設情況下,您應該安裝穩定版和夜間版工具鏈,但通常應盡可能使用穩定版。要安裝兩個工具鏈,請使用以下命令:

# 安裝穩定版 Rust 並將其設為預設工具鏈
$ rustup default stable

# 安裝夜間版 Rust
$ rustup toolchain install nightly

本文中的範例使用了 clippy 和 rustfmt,您可以使用 rustup 安裝它們:

$ rustup component add clippy rustfmt

切換預設工具鏈

在使用 Rust 時,您可能會頻繁在穩定版和夜間版工具鏈之間切換。rustup 使此切換相對容易:

# 切換到夜間版工具鏈
$ rustup override set nightly

透過遵循這些最佳實踐並避免常見的反模式,您可以編寫出更安全、更高效、更易於維護的 Rust 程式碼。持續實踐和參與 Rust 社群將有助於您進一步提升技能。