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