Rust 學習筆記:特徵

2023年10月18日 · 中文


前言

本系列文書寫我個人初探 Rust 的學習筆記,章節劃分主要基於著名的 The BookThe Rust Programming Language,程式碼部分通常是個人閱讀消化後以方便說明的方式撰寫,完整學習建議直接參見該書。

The Rust Programming Language

該書也有中文翻譯版,不過個人閱讀以英文原版為主以鞏固對 terminology 的一致認識,我認為對未來閱讀以及查找資料會較為順暢。

不論語言該書都是相當優秀的學習資源,選擇適合你的語言開始學習 Rust 吧。

Appendix F: Translations of the Book

特徵 Traits

特徵用於定義共同行為,類似於某些語言中 interface 的用途,但有些不同。

定義

定義 trait 的寫法如下:

trait Greeting {
    fn say_hello(&self) -> String;
}

impl 區塊非常像,不過這邊不寫方法的實作,而是提供簽名 (signatures),定義方法的名字、參數與回傳值。

實作

這邊是為 struct 實作 trait 的例子,在 impl 後面加上 for 以實作特徵。

trait Greeting {
    fn say_hello(&self) -> String;
}


struct Person {
    name: String
}

impl Greeting for Person {
    fn say_hello(&self) -> String {
        format!("Hello {}!", self.name)
    }
}

fn main() {
    let p1 = Person {
        name: String::from("Josh")
    };
    
    println!("{}", p1.say_hello());     // Hello Josh!
}

要注意的是實作必須包含所有 trait 所定義的簽名,否則會出現錯誤。

trait Greeting {
    fn say_hello(&self) -> String;
    fn say_goodbye(&self) -> String;    // added
}

struct Person {
    name: String
}

impl Greeting for Person {
    fn say_hello(&self) -> String {
        format!("Hello {}!", self.name)
    }
}

// ERROR: not all trait items implemented, missing: `say_bye`

預設實作

在 trait block 中並非不能撰寫實作部分,寫在 trait 中的實作會被用為預設實作。

trait Greeting {
    fn say_hello(&self) -> String;
    fn say_goodbye(&self) -> String {
        String::from("Good bye!")
    }
}

struct Person {
    name: String
}

impl Greeting for Person {
    fn say_hello(&self) -> String {
        format!("Hello {}!", self.name)
    }
}

fn main() {
    let p1 = Person {
        name: String::from("Josh")
    };
    
    println!("{}", p1.say_goodbye());   // Good bye!
}

用 trait 定參數

我們可以利用 trait 來定義參數的型別為有實作特定 trait 的類型,寫為 impl Trait 如下。

fn print_hello(p: &impl Greeting) {
    println!("{}", p.say_hello());
}

上面可以改用 trait bound 的寫法寫成下面這樣,這在介紹到泛型時用過。

fn print_hello<T: Greeting>(p: &T) {
    println!("{}", p.say_hello());
}

進階使用

  • 可以使用 + 來指定複數個 trait
  • 使用 where 語法定義 trait bound,避免冗長的泛型定義

下面程式碼建立了兩個 struct,其中 Person 實作了 HelloGoodbye trait,P 則只實作 Hello,利用上面兩個特性定義 greeting 函式。

struct Person {
    name: String
}

struct P {
    name: String
}

trait Hello {
    fn say_hello(&self) -> String {
        String::from("Hello!")
    }
}

trait Goodbye {
    fn say_goodbye(&self) -> String {
        String::from("Good bye!")
    }
}

impl Hello for Person {}
impl Goodbye for Person {}
impl Hello for P {}

fn greeting<T, U>(p1: &T, p2: &U)
where
    T: Hello + Goodbye,
    U: Hello
{
    println!("{}", p1.say_hello());
    println!("{}", p1.say_goodbye());
    
    println!("{}", p2.say_hello());
}

fn main() {
    let p1 = Person {
        name: String::from("Josh")
    };
    
    let p2 = P {
        name: String::from("Josh")
    };
    
    greeting(&p1, &p2);

    // output:
    // Hello!
    // Good bye!
    // Hello!
}

對所有有符合特徵的類型實作特徵

下面程式為有實作 Hello 的類型實作 PrintHello trait。

struct Person {
    name: String
}

trait Hello {
    fn say_hello(&self) -> String {
        String::from("Hello!")
    }
}

trait PrintHello {
    fn print_hello(&self);
}

impl Hello for Person {}
impl<T: Hello> PrintHello for T {
    fn print_hello(&self){
        println!("{}", self.say_hello());
    }
}

fn main() {
    let p1 = Person {
        name: String::from("Josh")
    };
    
    p1.print_hello();       // Hello!
}

寫在最後

泛型與特徵結合使用的技巧很多,這邊只有截選一些個人的筆記,掌握兩者並結合使用是減少程式碼重複的關鍵,也讓別人更易於使用自己寫的程式。

下一章是 Rust 中非常重要也非常不容易的 topic,生命週期 (lifetime)。