Rust 泛型之 Trait Object -靜態分發 - 動態分發-

通過上一篇文章 <<Rust 泛型之 Trait>> 的介紹,你現在可能想要知道,如何創建一個集合,裏邊包含了實現相同的 trait,但是是不同的具體類型。例如:

trait UsbModule {
    // ...
}
struct UsbCamera {
     // ...
}
impl UsbModule for UsbCamera {
    // ..
}
impl UsbCamera {
    // ...
}
struct UsbMicrophone{
     // ...
}
impl UsbModule for UsbMicrophone {
    // ..
}
impl UsbMicrophone {
    // ...
}
let peripheral_devices: Vec<UsbModule> = vec![
    UsbCamera::new(),
    UsbMicrophone::new(),
];

不幸的是,在 Rust 中不能這麼簡單的去實現。每個具體類型在內存中有不同的尺寸,編譯器不允許我們創建這樣的集合。

Trait Object 完美的解決了這個問題,在運行時才決定使用哪一個具體類型。

在我們的集合中,不直接使用對象,而是使用指向對象的指針。這時編譯器允許我們的代碼通過編譯,這是因爲指針是相同的尺寸。

在實踐中如何做到這一點?我們接着往下看:

靜態分發 vs 動態分發

泛型參數與 Trait Object 在技術上有什麼不同?我們通過例子來比較:

當使用泛型參數時:

trait Processor {
      fn compute(&self, x: i64, y: i64) -> i64;
}
struct Risc {}
impl Processor for Risc {
      fn compute(&self, x: i64, y: i64) -> i64 {
          x + y
      }
}
struct Cisc {}
impl Processor for Cisc {
      fn compute(&self, x: i64, y: i64) -> i64 {
          x + y
      }
}
fn process(processor: impl Processor, x: i64) {
      let result = processor.compute(x, 42);
      println!("{}", result);
}
fn main() {
    let processor1 = Risc {};
    let processor2 = Cisc {};
    process(processor1, 1);
    process(processor2, 2);
}

實際上,編譯器爲 Risc 和 Cisc 分別生成了不同版本的 process 函數,這就是所謂的單態化處理。上面的代碼大致相當於:

fn process_Risc(processor: &Risc, x: i64) {
    let result = processor.compute(x, 42);
    println!("{}", result);
}
fn process_Cisc(processor: &Cisc, x: i64) {
    let result = processor.compute(x, 42);
    println!("{}", result);
}

這就是靜態分發,在編譯期就進行了類型選擇,它提供了最好的運行時性能。

當使用 Trait Object 時:

trait Processor {
      fn compute(&self, x: i64, y: i64) -> i64;
}
struct Risc {}
  impl Processor for Risc {
    fn compute(&self, x: i64, y: i64) -> i64 {
        x + y
    }
}
struct Cisc {}
impl Processor for Cisc {
    fn compute(&self, x: i64, y: i64) -> i64 {
        x + y
    }
}
fn process(processor: Box<dyn Processor>, x: i64) {
    let result = processor.compute(x, 42);
    println!("{}", result);
}
fn main() {
    let processors: Vec<Box<dyn Processor>> = vec![
        Box::new(Risc{}), Box::new(Cisc{}),
    ];
    for processor in processors {
        process(processor, 1);
    }
}

編譯器只生成了一個 process 函數,在運行時才決定根據不同的 processor,調用相應的 compute 方法。這就是動態分發,在運行時動態的進行類型選擇。

關鍵點是編譯器需要知道已知大小的 processor。

在編譯動態分發的函數時,在 Rust 底層創建了一個虛表 (vtable),在運行時使用虛表來選擇應該調用哪個函數。

當你需要絕對的性能時,應該使用靜態分發;

當你需要靈活性及共享相同行爲的對象集合時,應該使用動態分發。

本文翻譯自:

https://kerkour.com/rust-generics-trait-objects

coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。

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