特徵

我們以前見過特徵(Trait):DebugCopyClone 都是特徵。要賦予型別特徵,就必須實作它。因為 Debug 和其他的特徵都很常見,所以我們有可以自動實作的屬性(attribute)。那就是當你寫下 #[derive(Debug)] 時所發生的事情:你自動實作了 Debug

#[derive(Debug)]
struct MyStruct {
    number: usize,
}

fn main() {}

但是其他的特徵就比較困難了,所以需要用 impl 手動實作。例如,Add (在 std::ops::Add 找到) 是用來累加兩個東西的。但是 Rust 並不知道你到底要怎麼累加,所以你必須告訴它。

struct ThingsToAdd {
    first_thing: u32,
    second_thing: f32,
}

fn main() {}

我們可以累加 first_thingsecond_thing,但我們需要提供更多資訊。也許我們想要 f32,所以像這樣:

#![allow(unused)]
fn main() {
// 🚧
let result = self.second_thing + self.first_thing as f32
}

但也許我們想要整數,所以像這樣:

#![allow(unused)]
fn main() {
// 🚧
let result = self.second_thing as u32 + self.first_thing
}

或者我們只是想把 self.first_thing 放在 self.second_thing 旁邊這樣加起來。所以如果我們把 55 加到 33.4,我們想看到的是 5533.4,而不是 88.4。

所以首先讓我們看一下如何做出特徵。trait 要記得的重點在於它們的行為 (behaviour)。要實作特徵時,寫下 trait,然後建立一些函式。

struct Animal { // 簡單結構體 - Animal只有名字
    name: String,
}

trait Dog { // 狗的特徵給出一些功能性
    fn bark(&self) { // 牠會吠叫
        println!("Woof woof!");
    }
    fn run(&self) { // 並且牠會跑
        println!("The dog is running!");
    }
}

impl Dog for Animal {} // 現在Animal有了特徵Dog

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark(); // Animal能用 bark()
    rover.run();  // 並且牠能用 run()
}

這範例沒問題,但是我們不想印出 "The dog is running"。如果你想的話,你可以更改 trait 給你的方法,但你必須有相同的簽名。這意味著它需要接受同樣的東西,並回傳同樣的東西。例如,我們可以改變 .run() 方法,但我們必須遵循簽名。簽名是:

#![allow(unused)]
fn main() {
// 🚧
fn run(&self) {
    println!("The dog is running!");
}
}

fn run(&self) 的意思是 "fn run() 接受 &self 引數,且不回傳任何內容"。所以你不能這樣做:

#![allow(unused)]
fn main() {
fn run(&self) -> i32 { // ⚠️
    5
}
}

Rust 會說:

   = note: expected fn pointer `fn(&Animal)`
              found fn pointer `fn(&Animal) -> i32`

但我們可以做這樣做:

struct Animal { // 簡單結構體 - Animal只有名字
    name: String,
}

trait Dog { // 狗的特徵給出一些功能性
    fn bark(&self) { // 牠會吠叫
        println!("Woof woof!");
    }
    fn run(&self) { // 並且牠會跑
        println!("The dog is running!");
    }
}

impl Dog for Animal {
    fn run(&self) {
        println!("{} is running!", self.name);
    }
}

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark(); // Animal能用 bark()
    rover.run();  // 並且牠能用 run()
}

現在印出了 Rover is running!。這樣可以是因為我們回傳的是 (),也就是什麼都沒有,也是特徵簽名所說的。

當你在寫特徵時,你可以只寫函式簽名,但如果你這樣做,使用者將必須寫出函式的實作內容。讓我們來試試。現在我們把 bark()run() 改成只有 fn bark(&self);fn run(&self);。這不是完整的函式,所以必須由使用者來寫。

struct Animal {
    name: String,
}

trait Dog {
    fn bark(&self); // bark() 說要有 &self 並且不回傳
    fn run(&self); // run() 說要有 &self 並且不回傳。
                   // 那麼現在我們必須要自己寫出它們。
}

impl Dog for Animal {
    fn bark(&self) {
        println!("{}, stop barking!!", self.name);
    }
    fn run(&self) {
        println!("{} is running!", self.name);
    }
}

fn main() {
    let rover = Animal {
        name: "Rover".to_string(),
    };

    rover.bark();
    rover.run();
}

所以當你建立特徵時,你必須思考:"我應該寫哪些函式?而使用者又應該寫哪些函式?"如果你認為使用者每次使用某個函式的方式應該一致,那麼就該把它寫出來。如果你認為使用者會有不同的使用方式,那就只寫出函式簽名即可。

那讓我們嘗試為我們的結構體實作 Display 特徵。首先我們將做個簡單的結構體:

struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };
}

現在我們想要印出 mr_mantle。Debug 很容易推導出:

#[derive(Debug)]
struct Cat {
    name: String,
    age: u8,
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    println!("Mr. Mantle is a {:?}", mr_mantle);
}

但 Debug 列印不是最漂亮的印出方式,因為它看起來像這樣。

Mr. Mantle is a Cat { name: "Reggie Mantle", age: 4 }

因此如果我們想要印出得更好看,就需要為 Cat 實作 Display。在 https://doc.rust-lang.org/std/fmt/trait.Display.html 上我們可以看到 Display 的資訊,還有一個範例。它說:

use std::fmt;

struct Position {
    longitude: f32,
    latitude: f32,
}

impl fmt::Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.longitude, self.latitude)
    }
}

fn main() {}

有些部分我們還不明白,比如 <'_>f 是做什麼的。但我們知道 Position 結構體:它只是兩個 f32。我們也懂 self.longitudeself.latitude 是結構體中的欄位。所以,也許我們可以拿這個程式碼來給我們的結構體用在 self.nameself.age 上。另外 write! 看起來很像 println!,所以會感到很熟悉。所以我們寫成這樣:

use std::fmt;

struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} is a cat who is {} years old.", self.name, self.age)
    }
}

fn main() {}

讓我們新增 fn main()。現在我們的程式碼像這樣:

use std::fmt;

struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
      write!(f, "{} is a cat who is {} years old.", self.name, self.age)
  }
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    println!("{}", mr_mantle);
}

成功了! 現在當我們使用 {} 列印時,我們得到 Reggie Mantle is a cat who is 4 years old.。這看起來好多了。

順帶一提,如果你實現了 Display,那麼你就可以免費得到 ToString 特徵。這是因為你使用 format! 巨集時間接使用了 .fmt() 函式,它讓你可以用 .to_string() 來做出 String。所以我們可以做類似這個範例做的事情,我們把 reggie_mantle 傳給想要 String 的函式,或者其他任何東西。

use std::fmt;
struct Cat {
    name: String,
    age: u8,
}

impl fmt::Display for Cat {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} is a cat who is {} years old.", self.name, self.age)
    }
}

fn print_cats(pet: String) {
    println!("{}", pet);
}

fn main() {
    let mr_mantle = Cat {
        name: "Reggie Mantle".to_string(),
        age: 4,
    };

    print_cats(mr_mantle.to_string()); // 這裡把牠轉換為 String
    println!("Mr. Mantle's String is {} letters long.", mr_mantle.to_string().chars().count()); // 把牠轉換成字元計數
}

印出:

Reggie Mantle is a cat who is 4 years old.
Mr. Mantle's String is 42 letters long.

關於特徵要記得的是,它們與某些東西的行為有關。你的 struct 是如何動作的?它能做什麼?這就是特徵的作用。如果你想想我們到目前為止所看到的一些特徵,它們全都是關於行為的:Copy 是型別可以做的事情。Display 也是型別能做的事情。ToString 是另一個特徵,它也是型別可以做的事情:它可以改變型別成為 String。在我們的 Dog 特徵中,Dog 這個詞並不意味著你能做的事情,但它給出了一些讓它做某些事情的方法。你也可以為 struct Poodlestruct Beagle 實作它,它們都會得到 Dog 的方法。

讓我們再看另一個更純粹是行為的範例。我們將想像一個有一些簡單角色的幻想遊戲。一個是 Monster,另外兩個是WizardRangerMonster 只是有 health,所以我們可以攻擊它,其他兩個還沒有任何東西。但是我們做了兩個特徵。一個叫 FightClose,讓你近身作戰。另一個是 FightFromDistance,讓你在遠處戰鬥。只有 Ranger 可以使用 FightFromDistance。它會像是這裡看到的這樣:

struct Monster {
    health: i32,
}

struct Wizard {}
struct Ranger {}

trait FightClose {
    fn attack_with_sword(&self, opponent: &mut Monster) {
        opponent.health -= 10;
        println!(
            "You attack with your sword. Your opponent now has {} health left.",
            opponent.health
        );
    }
    fn attack_with_hand(&self, opponent: &mut Monster) {
        opponent.health -= 2;
        println!(
            "You attack with your hand. Your opponent now has {} health left.",
            opponent.health
        );
    }
}
impl FightClose for Wizard {}
impl FightClose for Ranger {}

trait FightFromDistance {
    fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) {
        if distance < 10 {
            opponent.health -= 10;
            println!(
                "You attack with your bow. Your opponent now has {} health left.",
                opponent.health
            );
        }
    }
    fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) {
        if distance < 3 {
            opponent.health -= 4;
        }
        println!(
            "You attack with your rock. Your opponent now has {} health left.",
            opponent.health
        );
    }
}
impl FightFromDistance for Ranger {}

fn main() {
    let radagast = Wizard {};
    let aragorn = Ranger {};

    let mut uruk_hai = Monster { health: 40 };

    radagast.attack_with_sword(&mut uruk_hai);
    aragorn.attack_with_bow(&mut uruk_hai, 8);
}

印出:

You attack with your sword. Your opponent now has 30 health left.
You attack with your bow. Your opponent now has 20 health left.

我們總是在特徵裡傳入 self,但是我們現在還不能用它做什麼。那是因為 Rust 不知道什麼型別會使用它。它可能是一個 Wizard,也可能是一個 Ranger,也可能是一個叫做 Toefocfgetobjtnode 的新結構體,或者其他任何東西。為了讓 self 具有一定的功能,我們可以在特徵中加入必要的特徵。比如說,如果我們想用 {:?} 列印,那麼我們就需要 Debug。你只要把它寫在 :(冒號)後面,就可以把它加入到特徵中。現在我們的程式碼像這樣:

struct Monster {
    health: i32,
}

#[derive(Debug)] // 現在 Wizard 有 Debug
struct Wizard {
    health: i32, // 現在 Wizard 有 health
}
#[derive(Debug)] // Ranger 也是
struct Ranger {
    health: i32, // Ranger 也是
}

trait FightClose: std::fmt::Debug { // 現在型別需要有 Debug 來使用 FightClose
    fn attack_with_sword(&self, opponent: &mut Monster) {
        opponent.health -= 10;
        println!(
            "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}", // 我們現在可以用 {:?} 印出 self 因為我們有 Debug
            opponent.health, &self
        );
    }
    fn attack_with_hand(&self, opponent: &mut Monster) {
        opponent.health -= 2;
        println!(
            "You attack with your hand. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, &self
        );
    }
}
impl FightClose for Wizard {}
impl FightClose for Ranger {}

trait FightFromDistance: std::fmt::Debug { // 我們也可以加上特徵 FightFromDistance : FightClose, 因為 FightClose 需要 Debug
    fn attack_with_bow(&self, opponent: &mut Monster, distance: u32) {
        if distance < 10 {
            opponent.health -= 10;
            println!(
                "You attack with your bow. Your opponent now has {} health left.  You are now at: {:?}",
                opponent.health, self
            );
        }
    }
    fn attack_with_rock(&self, opponent: &mut Monster, distance: u32) {
        if distance < 3 {
            opponent.health -= 4;
        }
        println!(
            "You attack with your rock. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, self
        );
    }
}
impl FightFromDistance for Ranger {}

fn main() {
    let radagast = Wizard { health: 60 };
    let aragorn = Ranger { health: 80 };

    let mut uruk_hai = Monster { health: 40 };

    radagast.attack_with_sword(&mut uruk_hai);
    aragorn.attack_with_bow(&mut uruk_hai, 8);
}

現在印出:

You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left.  You are now at: Ranger { health: 80 }

在真實的遊戲中,為每個型別重寫印出內容可能比較好,因為 You are now at: Wizard { health: 60 } 看起來有點可笑。這也是為什麼特徵裡面的方法通常很簡單,因為你不知道什麼型別會使用它。例如,你不能寫出 self.0 += 10 這樣的東西。但是這個範例表明,我們可以在我們正在撰寫的特徵裡面使用其他的特徵。當我們這樣做的時候,我們會得到一些我們可以使用的方法。

另外一種使用特徵的方式是使用所謂的 特徵界限 (trait bound)。意思是"透過特徵進行限制"。特徵限制很簡單,因為特徵實際上不需要任何方法,或者說根本不需要任何東西。讓我們用類似但不同的東西重寫我們的程式碼。這次我們的特徵沒有任何方法,但我們有限定要使用的特徵的其它函式。

use std::fmt::Debug;  // 所以我們現在不用再每次寫 std::fmt::Debug

struct Monster {
    health: i32,
}

#[derive(Debug)]
struct Wizard {
    health: i32,
}
#[derive(Debug)]
struct Ranger {
    health: i32,
}

trait Magic{} // 這些特徵都沒有方法,它們只是特徵界限
trait FightClose {}
trait FightFromDistance {}

impl FightClose for Ranger{} // 每個型別都得到 FightClose,
impl FightClose for Wizard {}
impl FightFromDistance for Ranger{} // 但只有 Ranger 得到 FightFromDistance
impl Magic for Wizard{}  // 且只有 Wizard 得到 Magic

fn attack_with_bow<T: FightFromDistance + Debug>(character: &T, opponent: &mut Monster, distance: u32) {
    if distance < 10 {
        opponent.health -= 10;
        println!(
            "You attack with your bow. Your opponent now has {} health left.  You are now at: {:?}",
            opponent.health, character
        );
    }
}

fn attack_with_sword<T: FightClose + Debug>(character: &T, opponent: &mut Monster) {
    opponent.health -= 10;
    println!(
        "You attack with your sword. Your opponent now has {} health left. You are now at: {:?}",
        opponent.health, character
    );
}

fn fireball<T: Magic + Debug>(character: &T, opponent: &mut Monster, distance: u32) {
    if distance < 15 {
        opponent.health -= 20;
        println!("You raise your hands and cast a fireball! Your opponent now has {} health left. You are now at: {:?}",
    opponent.health, character);
    }
}

fn main() {
    let radagast = Wizard { health: 60 };
    let aragorn = Ranger { health: 80 };

    let mut uruk_hai = Monster { health: 40 };

    attack_with_sword(&radagast, &mut uruk_hai);
    attack_with_bow(&aragorn, &mut uruk_hai, 8);
    fireball(&radagast, &mut uruk_hai, 8);
}

印出來的東西幾乎一樣:

You attack with your sword. Your opponent now has 30 health left. You are now at: Wizard { health: 60 }
You attack with your bow. Your opponent now has 20 health left.  You are now at: Ranger { health: 80 }
You raise your hands and cast a fireball! Your opponent now has 0 health left. You are now at: Wizard { health: 60 }

所以你可以看到,當你使用特徵時,有很多方式可以做到同樣的事情。這一切都取決於什麼對你正在編寫的程式最有意義。

現在讓我們來看看如何實作一些你會在 Rust 中使用的主要特徵。

From 特徵

From 是個非常方便使用的特徵,你知道這一點是因為你已經看過很多遍。有了 From 你可以從 &str 做出 String,但你也可以用許多其他型別做出許多種型別。例如,Vec 能用 From 在以下型別:

From<&'_ [T]>
From<&'_ mut [T]>
From<&'_ str>
From<&'a Vec<T>>
From<[T; N]>
From<BinaryHeap<T>>
From<Box<[T]>>
From<CString>
From<Cow<'a, [T]>>
From<String>
From<Vec<NonZeroU8>>
From<Vec<T>>
From<VecDeque<T>>

那裡還有很多種 Vec::from() 我們還沒有嘗試用過。我們來用幾個看看會怎麼樣。

use std::fmt::Display; // 我們會做個用來印出它們的泛型函式,所以我們想要 Display

fn print_vec<T: Display>(input: &Vec<T>) { // 接受 Vec<T> 如果型別 T 有 Display
    for item in input {
        print!("{} ", item);
    }
    println!();
}

fn main() {

    let array_vec = Vec::from([8, 9, 10]); // 試著對陣列 from
    print_vec(&array_vec);

    let str_vec = Vec::from("What kind of vec will I be?"); // 對 &str from 的陣列? 這會蠻有趣的
    print_vec(&str_vec);

    let string_vec = Vec::from("What kind of vec will a String be?".to_string()); // 也是對 String 去 from
    print_vec(&string_vec);
}

印出的內容如下:

8 9 10
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 73 32 98 101 63
87 104 97 116 32 107 105 110 100 32 111 102 32 118 101 99 32 119 105 108 108 32 97 32 83 116 114 105 110 103 32 98 101 63

如果你觀察型別,第二個和第三個向量都是 Vec<u8>,也就是 &strString 的位元組。所以你可以看到 From 是非常靈活的,且用得很多。讓我們用自己的型別來試試看。

我們將做兩個結構體,然後為其中一個結構體實作 From。一個結構體會是 City,另一個結構體則會是 Country。我們希望能夠做到這件事:let country_name = Country::from(vector_of_cities)

它看起來像這樣:

#[derive(Debug)] // 這樣我們可以印出 City
struct City {
    name: String,
    population: u32,
}

impl City {
    fn new(name: &str, population: u32) -> Self { // 只是新的函式
        Self {
            name: name.to_string(),
            population,
        }
    }
}
#[derive(Debug)] // Country 也要可以被印出
struct Country {
    cities: Vec<City>, // 我們的城市都在這裡
}

impl From<Vec<City>> for Country { // 注意: 我們不用去寫 From<City>, 我們也可以改用
                                   // From<Vec<City>>. 因此我們也能實作在我們
                                   // 未曾建立的型別上
    fn from(cities: Vec<City>) -> Self {
        Self { cities }
    }
}

impl Country {
    fn print_cities(&self) { // 函式印出 Country 內的城市
        for city in &self.cities {
            // 用 & 因為 Vec<City> 不是 Copy
            println!("{:?} has a population of {:?}.", city.name, city.population);
        }
    }
}

fn main() {
    let helsinki = City::new("Helsinki", 631_695);
    let turku = City::new("Turku", 186_756);

    let finland_cities = vec![helsinki, turku]; // 這是 Vec<City>
    let finland = Country::from(finland_cities); // 所以現在我們能用 From

    finland.print_cities();
}

印出:

"Helsinki" has a population of 631695.
"Turku" has a population of 186756.

你可以看到,很容易從你沒有建立的型別中實作出 From,比如 Veci32 等等。這裡還有一個例子是,我們建立有兩個向量的向量。第一個向量存放偶數,第二個向量存放奇數。你可以用 From 給它一個 i32 的向量,它會把它變成 Vec<Vec<i32>>:一個向量裡面有許多容納 i32 的向量。

use std::convert::From;

struct EvenOddVec(Vec<Vec<i32>>);

impl From<Vec<i32>> for EvenOddVec {
    fn from(input: Vec<i32>) -> Self {
        let mut even_odd_vec: Vec<Vec<i32>> = vec![vec![], vec![]]; // 向量的裡面有兩個空向量
                                                                    // 這是回傳值但首先我們必須先將它填充
        for item in input {
            if item % 2 == 0 {
                even_odd_vec[0].push(item);
            } else {
                even_odd_vec[1].push(item);
            }
        }
        Self(even_odd_vec) // 現在它完成了那我們把它回傳為 Self (Self = EvenOddVec)
    }
}

fn main() {
    let bunch_of_numbers = vec![8, 7, -1, 3, 222, 9787, -47, 77, 0, 55, 7, 8];
    let new_vec = EvenOddVec::from(bunch_of_numbers);

    println!("Even numbers: {:?}\nOdd numbers: {:?}", new_vec.0[0], new_vec.0[1]);
}

印出:

Even numbers: [8, 222, 0, 8]
Odd numbers: [7, -1, 3, 9787, -47, 77, 55, 7]

EvenOddVec 這樣的型別可能最好是用泛型的 T,這樣我們就可以用在許多數值型別。如果你想練習的話,你可以試著把這個範例做成泛型的。

接受 String 和 &str 的函式

有時你想讓函式能同時接受 String&str。你可以透過泛型和 AsRef 特徵來做到這件事。AsRef 用於從某個型別向另一個型別提供參考。如果你查閱 String 文件,你可以看到它對許多型別都有提供 AsRef

https://doc.rust-lang.org/std/string/struct.String.html

這些是它們的一些函式簽名。

AsRef<str>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<str> for String

fn as_ref(&self) -> &str
}

AsRef<[u8]>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<[u8]> for String

fn as_ref(&self) -> &[u8]
}

AsRef<OsStr>:

#![allow(unused)]
fn main() {
// 🚧
impl AsRef<OsStr> for String

fn as_ref(&self) -> &OsStr
}

你可以看到,它接受 &self,並給出另一個型別的參考。這意味著,如果你有個泛型型別 T,你可以說它需要 AsRef<str>。如果你這樣做,它將會能夠接受 &strString

讓我們先從泛型函式說起。這個還不能執行:

fn print_it<T>(input: T) {
    println!("{}", input) // ⚠️
}

fn main() {
    print_it("Please print me");
}

Rust說 error[E0277]: T doesn't implement std::fmt::Display。所以我們會被要求給 T 實作 Display。

use std::fmt::Display;

fn print_it<T: Display>(input: T) {
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
}

現在可以執行並印出 Please print me。這不錯,但 T 仍然可以是太多種類的型別。它可以是 i8f32 及任何其它有 Display 的東西。所以我們加上 AsRef<str>,那麼現在 T 需要同時有實作 AsRef<str>Display

use std::fmt::Display;

fn print_it<T: AsRef<str> + Display>(input: T) {
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
    print_it("Also, please print me".to_string());
    // print_it(7); <- 這不會印出來
}

現在它不會接受像 i8 這樣的型別。

不要忘了,你可以在函式變長時用 where 以不一樣的方式寫出函式。如果我們加上 Debug,那麼它就會變成一整行長長的 fn print_it<T: AsRef<str> + Display + Debug>(input: T)。因此我們可以寫成這樣:

use std::fmt::{Debug, Display}; // 加上 Debug

fn print_it<T>(input: T) // 現在這行好讀多了
where
    T: AsRef<str> + Debug + Display, // 並且這些特徵也好讀
{
    println!("{}", input)
}

fn main() {
    print_it("Please print me");
    print_it("Also, please print me".to_string());
}