與 Rust 編譯器的鬥爭 - 5

讓我們來談談 dyn 和 impl 關鍵字以及'static 生命週期!

dyn 關鍵字用於表示實現給定某種類型 trait 的對象。這就是多態性的本質——我們不需要在編譯時知道具體的類型;我們只需要知道這個類型實現了這個 trait。當我們調用與 trait 關聯的方法時,它將被動態分派。由於具體對象是未知的,編譯器不能直接傳遞它,因爲它的大小是未知的。相反,它需要傳遞對象的指針或引用。這就是爲什麼 dyn 關鍵字通常與智能指針一起使用,如 Box

另一方面,impl 關鍵字表示具體類型在編譯時是已知的。這本質上是一個泛型類型,相當於 C++ 中的模板。編譯器確切地知道它是哪種類型,並可以直接傳遞對象。如果我們調用任何與 trait 相關的方法,它將被靜態分派。然而,由於具體類型是固定的,我們不能在運行時用不同的對象去替換對象。

思考下面的代碼:

trait Weapon {
    fn fire(&self);
}

struct M16Rifle;
impl Weapon for M16Rifle {
    fn fire(&self) {
        println!("dut dut");
    }
}

struct Bazuka;
impl Weapon for Bazuka {
    fn fire(&self) {
        println!("Kablam!!");
    }
}

struct Player {
    weapon: Box<dyn Weapon>,
}

impl Player {
    fn new(weapon: impl Weapon) -> Self {
        Self { weapon: Box::new(weapon) }
    }

    fn change_weapon(&mut self, weapon: impl Weapon) {
        self.weapon = Box::new(weapon);
    }

    fn shoot(&self) {
        self.weapon.fire();
    }
}

fn main() {
    let mut player = Player::new(M16Rifle);
    player.shoot();
    player.change_weapon(Bazuka);
    player.shoot();
}

玩家對象持有一些使用 Box 實現的武器對象。玩家的武器可以在運行時切換到不同的類型,例如,從 M16Rifle 切換到 Bazuka。代碼看起來很好,但是 Rust 編譯器給出了一個錯誤:

error[E0310]: the parameter type `impl Weapon` may not live long enough
  --> src/main.rs:25:24
   |
25 |         Self { weapon: Box::new(weapon) }
   |                        ^^^^^^^^^^^^^^^^ ...so that the type `impl Weapon` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound...
   |
24 |     fn new(weapon: impl Weapon + 'static) -> Self {
   |                                +++++++++

爲什麼編譯器要求添加'static 生命週期說明符?這可能看起來很可怕,因爲你不希望你的武器對象有一個 “靜態生命週期”。靜態生命週期對象佔用的內存直到程序結束時纔會被釋放,這可能會導致內存泄漏。

實際上,編譯器不是這麼說的。按照編譯器的建議指定'static 生命週期不會使武器突然變成靜態分配的。

代碼修改如下:

impl Player {
    fn new(weapon: impl Weapon + 'static) -> Self {
        Self { weapon: Box::new(weapon) }
    }

    fn change_weapon(&mut self, weapon: impl Weapon + 'static) {
        self.weapon = Box::new(weapon);
    }
    ......
}

相反,在方法簽名中添加'static 說明符是表示武器對象類型必須滿足以下要求之一:

1,它沒有與生命週期相關的變量。

2,它有一個與生命週期相關聯的變量,但這個生命週期是'static。

在我們的例子中,M16Rifle 和 Bazuka 結構體都沒有任何與生命週期相關的變量,因此它們滿足要求 1,這些對象在超出作用域時將被正常丟棄。事實上,當你寫 Box 時,它隱式地添加了'static 說明符 - Box<dyn Weapon'static>。

那麼,要求 2 什麼時候起作用呢?讓我們在遊戲中添加第三種武器:

impl<'a> CustomWeapon<'a> {
    fn new(sound: &'a str) -> Self {
        Self { sound }
    }
}

impl<'a> Weapon for CustomWeapon<'a> {
    fn fire(&self) {
        println!("{}", self.sound)
    }
}

fn main() {
    ......
    player.change_weapon(CustomWeapon::new("pew pew"));
    player.shoot();
}

我們的第三個武器:CustomWeapon<'a>,有一個生命週期說明符'a,因爲它有一個變量聲音,是一個 str 的引用。當我們用 CustomWeapon::new("pew pew") 創建這個武器時,我們提供的字面值字符串有一個'static 生命週期。因此,這個對象滿足要求 2,並且編譯得很好。

總之,在 Rust 中使用 dyn 和 impl 關鍵字可以實現靈活高效的代碼設計。dyn 關鍵字允許通過 trait 實現對不同具體類型的對象進行統一處理,從而實現多態性。另一方面,impl 關鍵字在編譯時提供靜態分派和精確的類型信息,允許直接對對象進行操作。理解生命週期在這些場景中的作用對於確保正確的代碼行爲至關重要。雖然添加'static 生命週期說明符似乎令人生畏,但它並不意味着靜態分配,而是表示對對象的生命週期有特定要求。通過有效地利用這些關鍵字和生命週期,Rust 程序員可以構建強大且可維護的代碼庫。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xe19mFm9KvuyBP1-ojfDyw