Deref 和 DerefMut

Deref 是讓你用 * 來對某些東西取值(dereference)的特徵。我們之前在使用元組結構體來做出新的型別時見過 Deref 這個字,現在是時候學會它了。

我們知道,參考和值是不一樣的:

// ⚠️
fn main() {
    let value = 7; // 這是個 i32
    let reference = &7; // 這是個 &i32
    println!("{}", value == reference);
}

而 Rust 連 false 都不給,因為它甚至不會比較兩者。

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src\main.rs:4:26
  |
4 |     println!("{}", value == reference);
  |                          ^^ no implementation for `{integer} == &{integer}`

當然,這裡的解法是使用 *。所以這將會印出 true

fn main() {
    let value = 7;
    let reference = &7;
    println!("{}", value == *reference);
}

現在讓我們想像一下只容納一個數字的簡單型別。它就像 Box,我們有些想法為它提供一些額外的功能。但如果我們只是給它一個數字,它就不能做那麼多了。

我們不能像使用 Box 那樣使用 *

// ⚠️
struct HoldsANumber(u8);

fn main() {
    let my_number = HoldsANumber(20);
    println!("{}", *my_number + 20);
}

錯誤訊息是:

error[E0614]: type `HoldsANumber` cannot be dereferenced
  --> src\main.rs:24:22
   |
24 |     println!("{:?}", *my_number + 20);

我們當然可以做到這一點:println!("{:?}", my_number.0 + 20);。但是這樣的話,我們就是在 20 的基礎上再單獨加 u8。如果我們能把它們直接加在一起就更好了。cannot be dereferenced 這個訊息給了我們線索:我們需要實作 Deref。實作 Deref 的簡單東西有時被稱為"智慧指標(smart pointer)"。一個智慧指標可以指向它的元素,有它的資訊,並且可以使用它的方法。因為現在我們可以新增 u8my_number.0,但我們不能用 HoldsANumber 來做其他的事情:到目前為止,它只有 Debug

有趣的事實是:String 其實是 &str 的智慧指標,Vec 是陣列(或其他型別)的智慧指標。所以我們其實從一開始就在使用智慧指標。

實現 Deref 並不難,標準函式庫中的範例也很簡單。這裡是標準函式庫中的範例程式碼

use std::ops::Deref;

struct DerefExample<T> {
    value: T
}

impl<T> Deref for DerefExample<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.value
    }
}

fn main() {
    let x = DerefExample { value: 'a' };
    assert_eq!('a', *x);
}

所以我們按照這個來,現在我們的 Deref 像這樣:

#![allow(unused)]
fn main() {
// 🚧
impl Deref for HoldsANumber {
    type Target = u8; // 記得, 這是"關聯型別(associated type)": 型別會一起寫在這.
                      // 你必須要使用正確的 type Target = (你想回傳的型別)

    fn deref(&self) -> &Self::Target { // 當你使用 * 時 Rust 會呼叫 .deref(). 我們只定義 Target 為 u8 所以這很容易理解
        &self.0   // 我們選擇 &self.0 因為這是元組結構體. 在具名結構體中它就會是像 "&self.number" 之類的東西
    }
}
}

所以現在我們可以用 * 來做:

use std::ops::Deref;
#[derive(Debug)]
struct HoldsANumber(u8);

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", *my_number + 20);
}

所以會印出 40,我們也不需要寫 my_number.0 了。這意味著我們有 u8 型別的方法可以用,我們可以為 HoldsANumber 寫出我們自己的方法。我們將新增自己寫的簡單方法,並使用我們從 u8 中得到的另一個方法,稱為 .checked_sub().checked_sub() 方法是安全的減法,它能回傳 Option。如果它能做減法,那麼它就會在 Some 裡面給你結果,如果它不能做減法,那麼它就會給你 None。記住,u8 不能是負數,所以還是 .checked_sub() 比較安全,這樣就不會恐慌了。

use std::ops::Deref;

struct HoldsANumber(u8);

impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let my_number = HoldsANumber(20);
    println!("{:?}", my_number.checked_sub(100)); // 這是來自 u8 的方法
    my_number.prints_the_number_times_two(); // 這是我們自己的方法
}

印出:

None
40

我們也可以實作 DerefMut,這樣我們就能透過 * 來改變數值。它看起來幾乎一樣。在實作 DerefMut 之前,你需要先實作 Deref

use std::ops::{Deref, DerefMut};

struct HoldsANumber(u8);

impl HoldsANumber {
    fn prints_the_number_times_two(&self) {
        println!("{}", self.0 * 2);
    }
}

impl Deref for HoldsANumber {
    type Target = u8;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl DerefMut for HoldsANumber { // 這裡你不需要 type Target = u8; 這要感謝 Deref 因為它已經知道了
    fn deref_mut(&mut self) -> &mut Self::Target { // 除了到處用 mut 以外,其它一切都一樣
        &mut self.0
    }
}

fn main() {
    let mut my_number = HoldsANumber(20);
    *my_number = 30; // DerefMut lets us do this
    println!("{:?}", my_number.checked_sub(100));
    my_number.prints_the_number_times_two();
}

所以你可以看到,Deref 給你的型別提供了強大的力量。

這也是為什麼標準函式庫說:Deref should only be implemented for smart pointers to avoid confusion。這是因為對於複雜的型別,你可以用 Deref 做一些奇怪的事情。讓我們想像一個非常混亂的範例來理解它們的含義。我們將從一個遊戲的 Character 結構體開始。新的 Character 需要一些資料,比如智力和力量。所以這裡是我們的第一個角色:

struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    health: u8,
    intelligence: u8,
    wisdom: u8,
    charm: u8,
    hit_points: i8,
    alignment: Alignment,
}

impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        health: u8,
        intelligence: u8,
        wisdom: u8,
        charm: u8,
        hit_points: i8,
        alignment: Alignment,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            health,
            intelligence,
            wisdom,
            charm,
            hit_points,
            alignment,
        }
    }
}

enum Alignment {
    Good,
    Neutral,
    Evil,
}

fn main() {
    let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good);
}

現在讓我們想像我們想存放人物的生命值(hit points)在一個大向量裡面。也許我們也會把怪物級資料也放進去,並存放在一起。由於 hit_pointsi8,我們實作了 Deref,來讓我們可以對它進行各式各樣的數學計算。但是現在看看我們的 main() 函式有多麼奇怪:

use std::ops::Deref;

// 直到例舉 Alignment 之後,以外的所有程式碼是一樣的
struct Character {
    name: String,
    strength: u8,
    dexterity: u8,
    health: u8,
    intelligence: u8,
    wisdom: u8,
    charm: u8,
    hit_points: i8,
    alignment: Alignment,
}

impl Character {
    fn new(
        name: String,
        strength: u8,
        dexterity: u8,
        health: u8,
        intelligence: u8,
        wisdom: u8,
        charm: u8,
        hit_points: i8,
        alignment: Alignment,
    ) -> Self {
        Self {
            name,
            strength,
            dexterity,
            health,
            intelligence,
            wisdom,
            charm,
            hit_points,
            alignment,
        }
    }
}

enum Alignment {
    Good,
    Neutral,
    Evil,
}

impl Deref for Character { // 給 Character 實作 Deref. 現在我們可以任意做整數計算!
    type Target = i8;

    fn deref(&self) -> &Self::Target {
        &self.hit_points
    }
}



fn main() {
    let billy = Character::new("Billy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good); // 建立兩個角色, billy 和 brandy
    let brandy = Character::new("Brandy".to_string(), 9, 8, 7, 10, 19, 19, 5, Alignment::Good);

    let mut hit_points_vec = vec![]; // 把我們的生命值資料放在這裡
    hit_points_vec.push(*billy);     // 推入 *billy?
    hit_points_vec.push(*brandy);    // 推入 *brandy?

    println!("{:?}", hit_points_vec);
}

印出 [5, 5]。我們的程式碼現在讓人讀起來感覺非常奇怪。我們可以讀懂在 main() 上面的 Deref,然後弄清楚 *billy 的意思是 i8,但是如果有很多程式碼呢?可能我們的程式碼長 2000 行,並且突然之間我們要弄清楚為什麼要 .push() *billyCharacter 當然不僅僅是 i8 的智慧指標。

當然寫 hit_points_vec.push(*billy) 並不違法,但這讓程式碼看起來非常奇怪。也許簡單的 .get_hp() 方法會好得多,或者另一個存放角色的結構體。然後你可以疊代並推入每個角色的 hit_pointsDeref 雖然提供了強大的力量,但最好確保程式碼的邏輯性。