在 Rust 中,我們經常需要處理未使用的泛型引數,尤其是在定義結構體時。如果泛型引數未被使用,編譯器會發出警告。為了消除這個警告,我們可以使用 PhantomData 來標記這些未使用的泛型引數,告訴編譯器我們需要保留型別資訊,但不需要實際儲存它。PhantomData 是一種零大小型別,不佔用任何記憶體空間,它主要用於編譯時型別檢查。透過 PhantomData,我們可以定義一個帶有泛型引數的結構體,即使該引數在結構體內部並沒有被直接使用,也能夠避免編譯器警告,並在需要時利用該泛型引數進行型別推導和特質實作。例如,我們可以定義一個 Dog 結構體,其中包含一個 Breed 泛型引數,並使用 PhantomData<Breed> 來標記它。即使 Breed 沒有在 Dog 結構體內部被使用,我們仍然可以在特質實作中利用它,例如為不同品種的 Dog 實作不同的 breed_name 方法。這種技巧在處理需要型別資訊但不需實際儲存值的場景下非常有用。

2.1.5 使用PhantomData進行泛型引數標記

在Rust中,泛型引數的使用需要謹慎處理,特別是在結構體(struct)中宣告泛型引數時。考慮以下例子:

struct Dog<Breed> {
    name: String,
}

編譯器會對此程式碼提出警告,因為Breed引數並未被使用:

warning: unused parameter `Breed`
  |
  = help: consider removing `Breed`, referring to it in a field,
          or using a marker such as `PhantomData`
  = help: if you intended `Breed` to be a const parameter,
          use `const Breed: usize` instead

此警告提示我們,Breed引數未被使用。為瞭解決這個問題,我們可以使用PhantomData來標記這個泛型引數,讓編譯器知道我們需要在編譯時保留這個型別資訊,但不需要在執行時儲存它。

使用PhantomData解決未使用的泛型引數

use std::marker::PhantomData;

struct Dog<Breed> {
    name: String,
    breed: PhantomData<Breed>,
}

在這個修改後的版本中,我們引入了PhantomData並將其加入到Dog結構體中。這樣做的目的是告訴編譯器,我們需要保留Breed的型別資訊,但不需要實際儲存它。

建立Dog例項

建立Dog例項時,我們仍然需要提供PhantomData

let my_poodle: Dog<Poodle> = Dog {
    name: "Jeffrey".into(),
    breed: PhantomData,
};

PhantomData的作用

PhantomData是一種特殊的標記,用於在編譯時保留型別資訊。它不會佔用實際的記憶體空間,因為它是一個零大小的型別(Zero-Sized Type, ZST)。這使得它非常適合用於需要在編譯時進行型別檢查,但不需要在執行時儲存該型別的場景。

為不同品種的Dog實作特定方法

透過使用泛型和PhantomData,我們可以為不同品種的Dog實作特定方法,而無需在結構體中實際儲存品種資訊。例如:

impl Dog<Labrador> {
    fn breed_name(&self) -> &str {
        "labrador"
    }
}

impl Dog<Poodle> {
    fn breed_name(&self) -> &str {
        "poodle"
    }
}

這裡,我們為Dog<Labrador>Dog<Poodle>分別實作了breed_name方法,這些方法傳回對應的品種名稱。

測試我們的程式碼

最後,我們可以測試我們的程式碼:

let my_poodle: Dog<Poodle> = Dog {
    name: "Jeffrey".into(),
    breed: PhantomData,
};

println!(
    "My dog is a {}, named {}",
    my_poodle.breed_name(),
    my_poodle.name,
);

輸出結果為:

My dog is a poodle, named Jeffrey
2.1.6 泛型引數的特徵約束

在討論完泛型和PhantomData的使用後,我們接下來討論泛型引數的特徵約束(trait bounds)。特徵約束允許我們對泛型引數施加限制,指定它們必須實作哪些特徵。

特徵約束的基本用法

考慮以下例子:

#[derive(Clone)]
struct ListItem<T>
where
    T: Clone + Debug,
{
    data: Box<T>,
    next: Option<Box<ListItem<T>>>,
}

在這個例子中,我們對T施加了兩個特徵約束:CloneDebug。這意味著任何用於替換T的型別都必須實作這兩個特徵。

為什麼需要特徵約束?

特徵約束是為了確保泛型程式碼的安全性和可用性。透過指定泛型引數必須實作哪些特徵,我們可以保證我們的程式碼能夠正確地處理這些型別。

2.2 特徵(Traits)

在Rust中,特徵(traits)是一種定義分享行為的方式。特徵可以被視為一種介面(interface),它定義了一組方法,這些方法可以被多種型別實作。

特徵的基本用法

一個簡單的特徵定義如下:

trait MyTrait {
    fn my_method(&self);
}

任何實作了MyTrait的型別都必須提供my_method的實作。

特徵與泛型的結合使用

特徵與泛型結合使用,可以提供非常強大的抽象能力。例如,我們可以定義一個泛型函式,它接受任何實作了特定特徵的型別:

fn my_function<T: MyTrait>(t: T) {
    t.my_method();
}

這個函式可以接受任何實作了MyTrait的型別作為引數。

2.2 特質(Traits):Rust 程式設計的核心要素

特質(Traits)是 Rust 程式設計的根本,它提供了強大的抽象能力,使 Rust 的函式庫設計更為靈活。然而,這種強大的功能也伴隨著責任,因為特質的使用需要謹慎處理,以避免特質汙染(trait pollution)和特質重複定義(trait duplication)等問題。

2.2.1 為何特質不是物件導向程式設計?

Rust 並不是一門物件導向程式語言(OOP),但其語法在某些方面與 OOP 語言相似。Rust 具有物件的概念,物件可以包含狀態(如結構體或列舉),並且可以呼叫方法。然而,Rust 缺乏 OOP 語言中的一個重要特性:繼承(inheritance)。

Rust 使用特質來替代繼承。特質與類別(或類別繼承)並不相同,但它們解決了類別似的問題。在 OOP 中,物件透過繼承來擴充套件功能;而在根據特質的程式設計中,特質可以被新增到任何結構或資料型別上,為其提供特定的功能。

物件繼承定義了「is-a」關係,而特質則定義了功能。換句話說,特質在不同的狀態之上新增分享的功能,而這些功能並不與特定的型別(或狀態)耦合。

程式碼範例:C++ 中的繼承 vs Rust 中的特質

// C++ 示例
class Rectangle {
protected:
    int width;
    int height;
public:
    Rectangle(int width, int height) : width(width), height(height) {}
    int get_area() { return width * height; }
    int get_width() { return width; }
    int get_height() { return height; }
};

class Square : public Rectangle {
public:
    Square(int length) : Rectangle(length, length) {}
    int get_length() { return width; }
};

對應的 Rust 程式碼如下:

// Rust 示例
struct Rectangle {
    width: i32,
    height: i32,
}

impl Rectangle {
    fn get_area(&self) -> i32 {
        self.width * self.height
    }

    fn get_width(&self) -> i32 {
        self.width
    }

    fn get_height(&self) -> i32 {
        self.height
    }
}

struct Square {
    length: i32,
}

impl Square {
    fn new(length: i32) -> Self {
        Square { length }
    }

    fn get_length(&self) -> i32 {
        self.length
    }

    fn get_area(&self) -> i32 {
        self.length * self.length
    }
}

內容解密:

在 C++ 示例中,Square 類別繼承自 Rectangle 類別,利用了物件導向程式設計中的繼承機制。然而,在 Rust 中,我們並不使用繼承,而是為 RectangleSquare 分別實作各自的方法。這種方式更符合 Rust 的特質系統,提供了更大的靈活性。

2.2.2 特質的組成要素

特質由定義和零或多個可選的實作組成。一個特質定義通常包含以下部分:

  • 特質名稱
  • 一組可選的方法(可帶有預設實作)
  • 可選的泛型型別引數
  • 可選的一組必要特質

最簡單的特質定義只需要一個名稱,如下所示:

trait MinimalTrait {}

特質實作

特質實作將特質的定義應用於特定的型別。下面是一個簡單的特質及其實作範例:

trait DoesItBark {
    fn it_barks(&self) -> bool;
}

struct Dog;

impl DoesItBark for Dog {
    fn it_barks(&self) -> bool {
        true
    }
}

內容解密:

在這個範例中,我們定義了一個名為 DoesItBark 的特質,該特質包含一個方法 it_barks。接著,我們為 Dog 結構體實作了 DoesItBark 特質,並傳回 true,因為狗會吠叫。

2.2.3 透過物件導向程式碼理解特質

相較於繼承,特質提供了更大的靈活性。繼承要求一種自下而上的關係,即在層次結構的較低層級定義分享行為。讓我們先考慮一個 C++ 的範例,然後再看看如何在 Rust 中實作相同的功能。

C++ 範例:

// 對應的 C++ 程式碼如前所示

Rust 範例:

// 對應的 Rust 程式碼如前所示

內容解密:

在 C++ 中,Square 繼承自 Rectangle,而在 Rust 中,我們分別為 RectangleSquare 實作各自的方法和特質。這種方式避免了嚴格的繼承關係,使得程式碼更具彈性。

特質與繼承的比較

  graph TD;
    A[物件導向程式設計] -->|繼承|> B[類別層次結構];
    A -->|特質|> C[靈活的功能擴充套件];
    B -->|僵化|> D[不易擴充套件];
    C -->|靈活|> E[易於擴充套件];

圖表翻譯:

此圖表比較了物件導向程式設計中的繼承與 Rust 中的特質系統。繼承導致類別層次結構較為僵化,而特質則提供了更靈活的功能擴充套件方式。

在未來的章節中,我們將進一步探討 Rust 中的進階特質使用,包括泛型特質、特質界限等主題,以幫助讀者更深入地理解 Rust 的特質系統。

參考資料

練習題

  1. 請實作一個簡單的特質,並為某個結構體實作該特質。
  2. 試著將一個 C++ 的物件導向程式碼轉換為 Rust 中的特質實作。

本章結束

本章對 Rust 中的特質進行了詳細介紹,包括其基本概念、與物件導向程式設計的比較,以及實際的程式碼範例。希望讀者能夠透過本章的內容,更好地理解並應用 Rust 中的特質。

Rust 基本構建區塊:Traits 與泛型的應用

在 Rust 程式語言中,Traits 扮演著介面的角色,用於定義分享行為。本章節將探討 Traits 的基本概念、實作方式,以及如何結合泛型(Generics)來提升程式碼的靈活性與可擴充套件性。

實作矩形與正方形

首先,我們定義兩個基本的結構:RectangleSquare。這兩個結構分別代表矩形和正方形,並各自實作了 new 方法用於建立例項。

struct Rectangle {
    width: i32,
    height: i32,
}

impl Rectangle {
    pub fn new(width: i32, height: i32) -> Self {
        Self { width, height }
    }
}

struct Square {
    length: i32,
}

impl Square {
    pub fn new(length: i32) -> Self {
        Self { length }
    }

    pub fn get_length(&self) -> i32 {
        self.length
    }
}

內容解密:

  • Rectangle 結構包含 widthheight 兩個屬性,用於表示矩形的寬度和高度。
  • Square 結構僅包含一個 length 屬性,因為正方形的所有邊長相等。
  • new 方法是一種常見的建構函式模式,用於建立新的結構例項。
  • get_length 方法提供了一個存取器,用於取得正方形的邊長。

定義與實作 Rectangular Trait

接下來,我們定義了一個名為 Rectangular 的 Trait,用於提供對矩形和正方形共通屬性的存取。

pub trait Rectangular {
    fn get_width(&self) -> i32;
    fn get_height(&self) -> i32;
    fn get_area(&self) -> i32;
}

impl Rectangular for Rectangle {
    fn get_width(&self) -> i32 {
        self.width
    }

    fn get_height(&self) -> i32 {
        self.height
    }

    fn get_area(&self) -> i32 {
        self.width * self.height
    }
}

impl Rectangular for Square {
    fn get_width(&self) -> i32 {
        self.length
    }

    fn get_height(&self) -> i32 {
        self.length
    }

    fn get_area(&self) -> i32 {
        self.length * self.length
    }
}

內容解密:

  • Rectangular Trait 定義了三個方法:get_widthget_heightget_area,用於取得寬度、高度和麵積。
  • 我們為 RectangleSquare 分別實作了 Rectangular Trait,提供了具體的實作邏輯。
  • 雖然這裡看似重複實作了相同的邏輯,但它展示了 Trait 的靈活性,能夠讓不同型別分享相同的介面。

UML 圖示

此圖示呈現了 Rectangular Trait 及其相關結構之間的關係。

  classDiagram
    class Rectangular {
        <<interface>>
        +get_width()
        +get_height()
        +get_area()
    }
    class Rectangle {
        +get_width()
        +get_height()
        +get_area()
    }
    class Square {
        +get_length()
        +get_width()
        +get_height()
        +get_area()
    }
    Rectangle ..&lt;| Rectangular
    Square ..&lt;| Rectangular

圖表翻譯: 此圖表展示了 Rectangular Trait 與其實作類別 RectangleSquare 之間的關係。RectangleSquare 都實作了 Rectangular Trait,提供了寬度、高度和麵積的計算方法。

測試程式碼

最後,我們編寫了一個簡單的測試函式來驗證我們的實作。

fn main() {
    let rect = Rectangle::new(2, 3);
    let square = Square::new(5);
    
    println!("rect has width {}, height {}, and area {}", 
             rect.get_width(), 
             rect.get_height(), 
             rect.get_area());
    
    println!("square has length {} and area {}", 
             square.get_length(), 
             square.get_area());
}

內容解密:

  • main 函式中,我們建立了一個 Rectangle 和一個 Square 的例項。
  • 透過呼叫 Rectangular Trait 中定義的方法,我們可以取得並列印預出矩形和正方形的屬性。

結合泛型與 Traits

Rust 的強大之處在於能夠結合泛型與 Traits,建立出高度靈活的程式碼。假設我們想要編寫一個函式,能夠接受任意型別並傳回該型別的描述。我們可以定義一個泛型函式,並使用 Trait Bound 來限制型別必須實作特定的 Trait。

pub trait SelfDescribing {
    fn describe(&self) -> String;
}

fn describe_type<T>(t: &T) -> String
where
    T: SelfDescribing,
{
    t.describe()
}

內容解密:

  • 我們定義了一個名為 SelfDescribing 的 Trait,其中包含一個 describe 方法,用於傳回型別的描述。
  • describe_type 函式接受一個泛型引數 T,並要求 T 必須實作 SelfDescribing Trait。
  • 這樣,我們可以確保傳入的任何型別都能夠提供描述自身的資訊。

實際應用

為了測試上述功能,我們定義了兩個結構:DogCat,並為它們實作了 SelfDescribing Trait。

struct Dog;
struct Cat;

impl SelfDescribing for Dog {
    fn describe(&self) -> String {
        String::from("a dog")
    }
}

impl SelfDescribing for Cat {
    fn describe(&self) -> String {
        String::from("a cat")
    }
}

fn main() {
    let dog = Dog;
    let cat = Cat;
    
    println!("I am {}", describe_type(&dog));
    println!("I am {}", describe_type(&cat));
}

內容解密:

  • 我們為 DogCat 分別實作了 SelfDescribing Trait,提供了具體的描述資訊。
  • main 函式中,我們建立了 DogCat 的例項,並透過 describe_type 函式列印預出它們的描述。