dyn compatibility
本集目標
理解哪些 trait 可以用 dyn、哪些不行,以及背後的原因。
概念說明
不是所有 trait 都能用 dyn
上一集學了 dyn Trait。但如果你嘗試寫 dyn Clone,會得到編譯錯誤。這是因為 Clone 不是 dyn compatible 的。
核心概念:impl Trait for dyn Trait
要理解 dyn compatibility,先想想 dyn Trait 是怎麼運作的。編譯器自動生成了一個:
impl Trait for dyn Trait {
fn method(&self, ...) {
// 查 vtable,呼叫實際的實作
}
}
在這個自動生成的 impl 裡,Self = dyn Trait。而 dyn Trait 是 DST——大小不固定、不是 Sized。
如果 trait 的某些方法在 Self = dyn Trait 的情況下沒辦法運作,這個 trait 就不是 dyn compatible 的。具體來說有以下幾種情況:
限制一:Self 不能出現在 self 之外的型別中
trait Compare {
fn compare(&self, other: &Self) -> bool;
}
impl Compare for Cat {
fn compare(&self, other: &Cat) -> bool { ... }
}
impl Compare for Dog {
fn compare(&self, other: &Dog) -> bool { ... }
}
compare 的第二個參數是 &Self。當你用 dyn Compare 的時候,具體型別已經被抹掉了——你不知道裡面是 Cat 還是 Dog。但 Cat::compare 期望的是 &Cat,Dog::compare 期望的是 &Dog。
如果有人傳了一個 Dog 進來,但 vtable 找到的函數是 Cat::compare,函數就會把 Dog 的資料當成 Cat 來用——型別搞混了。
要確保不搞混,編譯器就需要在執行期檢查「傳進來的 y 的具體型別跟 x 的具體型別一樣」。但 dyn 的重點就是把具體型別抹掉了,編譯器已經不知道原本是什麼型別,沒辦法做這個檢查。所以 Rust 直接禁止這樣做。
限制二:方法不能有泛型參數
trait Converter {
fn convert<U>(&self) -> U;
}
fn main() {}
vtable 是一張固定大小的函數指標表。但泛型方法對每個不同的 U 都是一個不同的函數——convert::<i32> 和 convert::<String> 是兩個不同的函數指標。vtable 沒辦法塞進無限多個版本。
主要是 vtable 必須在編譯 impl 的那一方建好——因為只有那邊才知道 Self 的具體型別。但編譯 impl 的時候,你不知道使用者之後會用哪些 U,所以 vtable 不可能提前準備好所有版本。
限制三:trait 本身不能要求 Self: Sized
回頭看開頭的問題——為什麼 dyn Clone 不行?除了回傳 Self,其實 Clone 有一個 supertrait 就是 Sized:
trait Clone: Sized {
fn clone(&self) -> Self;
}
fn main() {}
Clone: Sized 代表「實作 Clone 的型別必須是 Sized」。但 dyn Clone 是 DST,不是 Sized。所以 impl Clone for dyn Clone 根本不成立,dyn Clone 無法存在。
退出機制:where Self: Sized
如果一個 trait 只有一部分的方法是 dyn compatible,其他不是,可以在那些其他方法上全部加 where Self: Sized 讓它們退出:
trait MyTrait {
fn normal(&self) -> String; // dyn MyTrait 上能呼叫
fn special(&self) -> Self // 回傳 Self,不 dyn compatible
where Self: Sized; // 加上這個,讓它退出
}
fn main() {}
where Self: Sized 的意思是「只有 Self 是 Sized 的時候才能呼叫這個方法」。dyn MyTrait 不是 Sized,所以這個方法在 dyn MyTrait 上不能呼叫——但 trait 本身還是 dyn compatible 的,其他方法仍然能透過 dyn MyTrait 使用。
let x: &dyn MyTrait = &something;
x.normal(); // OK
// x.special(); // 編譯錯誤:dyn MyTrait 不是 Sized
dyn compatibility 的完整規則其實比這集講的更複雜,但八九不離十就是這些了。
範例程式碼
// dyn compatible 的 trait
trait Greet {
fn greet(&self) -> String;
}
struct Alice;
struct Bob;
impl Greet for Alice {
fn greet(&self) -> String { String::from("Hi, I'm Alice!") }
}
impl Greet for Bob {
fn greet(&self) -> String { String::from("Hey, I'm Bob!") }
}
// 混合使用 where Self: Sized 的 trait
trait Animal {
fn name(&self) -> &str;
// 這個方法不 dyn compatible(回傳 Self),用 where Self: Sized 退出
fn duplicate(&self) -> Self
where
Self: Sized + Clone;
}
#[derive(Clone)]
struct Cat { name: String }
impl Animal for Cat {
fn name(&self) -> &str { &self.name }
fn duplicate(&self) -> Self
where
Self: Sized + Clone,
{
self.clone()
}
}
fn main() {
// dyn Greet:不同型別放在同一個 Vec
let greeters: Vec<Box<dyn Greet>> = vec![
Box::new(Alice),
Box::new(Bob),
];
for g in &greeters {
println!("{}", g.greet());
}
// dyn Animal:name() 能用,duplicate() 不能用
let cat = Cat { name: String::from("小花") };
let animal: &dyn Animal = &cat;
println!("動物:{}", animal.name()); // OK
// animal.duplicate(); // 編譯錯誤:dyn Animal 不是 Sized
// 但用具體型別就能呼叫 duplicate
let cat2 = cat.duplicate();
println!("複製:{}", cat2.name());
}
重點整理
- 不是所有
trait都能用dyn——必須是dyncompatible 的 - 核心概念:編譯器自動生成
impl Trait for dyn Trait,Self=dyn Trait(DST) Self不能出現在self之外的型別中——具體型別已被抹掉- 方法不能有泛型參數——vtable 固定大小,放不下無限多版本
- trait 不能要求
Self: Sized——dyn Trait是 DST,不是Sized - 個別方法加
where Self: Sized可以讓它退出dyn,trait本身仍然dyncompatible