Rust 中的 dyn Trait 語法會生成一種無大小 (Unsized !Sized) 類型,以符合動態分派的機制,也就是讓物件以 trait 的形式描述,而非其實際類型。身為一種無大小類型,只有其指標才是有大小類型,而且會是一個胖指標 (fat pointer)。以下是 Rust 目前胖指標的定義。
/// Slice<'a, T> == &'a [T]
struct Slice<'a, T> {
ptr: *const T,
len: usize,
_marker: PhantomData<'a>,
}
/// DynTraitRef<'a> == &'a dyn Trait
struct DynTraitRef<'a> {
ptr: *const (), // 假設 vtable 指對,*const () 轉 *const Implementor 會是類型安全的
vtable: *const TraitVTable,
_marker: PhantomData<'a>,
}由於 [T] 和 dyn Trait 的指標都是兩個指標或 usize 組成,大小會是一般指標的兩倍。不過相較於簡單的 [T],dyn 物件需要產生一組 vtable struct,將每個 trait method 的 function pointer,正確地指到該物件類型真正實作的那個 function,這邊 Rust 都會自動處理。
trait MyTrait {
fn method1(&self) -> bool;
fn method2(&mut self);
}
struct MyType;
impl MyTrait for MyType { .. }
mod compiler_intrinsics {
struct MyTraitVTable {
method1: fn(*const ()) -> bool,
method2: fn(*mut ()),
}
const MYTRAIT_VTABLE_FOR_MYTYPE: MyTraitVTable = MyTraitVTable {
method1: compiler_cast(<MyType as MyTrait>::method1),
method2: compiler_cast(<MyType as MyTrait>::method2),
};
}
let obj = MyType;
let ptr = &obj as &dyn MyTrait; // MYTRAIT_VTABLE_FOR_MYTYPE 將會用在這裡不過,dyn Trait 語法有一些限制:
- 首先是語法僅限
dyn Trait1 + AutoTrait1 + AutoTrait2 + ...,只允許一個手動 trait 當作 vtable,其他都只能是自動 trait(例如 Send、Sync 等)。這表示 vtable 只能由一個手動 trait 和其繼承的 trait 產生。 dyn Trait其實隱藏了一個 bound,為dyn Trait + Sized。這是因為胖指標不應該再包含胖指標,否則無法統一&dyn Trait的大小。換句話說,就是上面的DynTraitRef::ptr欄位必須是普通指標。dyn Trait類型實作了Trait,所以Trait必須是未知大小的 (?Sized),不可以在宣告時寫trait Trait: Sized {}。要繞過這個限制,我們可以用where Self: Sized加在 method 後面,這樣 VTable 不會生成需要特定 Self 條件的 function。- Trait 的關聯類型 (associated types) 必須是已知的,才能產生 vtable 的 function pointer。例如
&dyn mut Iterator<Item = u8>。 - Trait 若沒有繼承
Sized、每一個 method 都以指標類型&self/&mut self/self: Arc<Self>/self: Pin<&mut Self>當作第一輸入、沒有回傳無指標的Self類型,便符合dyncompatible 的條件。簡單來說就是能讓!Sized順利實作的 trait。當然,它繼承的 trait 也必須是dyncompatible 的。 - 可以向上轉型
dyn Trait變成dyn SuperTrait(變成較少功能的 trait)。關係是Trait: SuperTrait。
符合 dyn compatible 的 trait,可以讓它的已知大小的 implementor,使用 &obj as &dyn Trait 轉型語法,能任意指定其 auto trait,變成 dyn Trait 類型使用。其指標類型,例如 &dyn Trait、Box<dyn Trait>、Arc<dyn Trait> 等等,都可以拿來跟實作相同 trait 的物件混合使用,而不用知道其實際類型。這在像 closure、iterator、future 等狀態機物件都非常實用,其中由於 closure 是語法糖產生的類型,是沒有類型名稱可以描述的,因此只能用 impl Trait 或 dyn Trait 的方式描述來傳遞和儲存。以下是一個簡單混裝類型的範例。
trait MyTrait: std::fmt::Debug {}
impl MyTrait for u64 {}
impl MyTrait for bool {}
impl MyTrait for () {}
let mut vec = Vec::<Box<dyn MyTrait>>::new();
vec.push(Box::new(()));
vec.push(Box::new(10));
vec.push(Box::new(false));
println!("{vec:?}"); // [(), 10, false]Warning
當 trait 包含 Self 輸入或回傳時,沒有經過指標的包裝,就必須要有 Self: Sized 的限制。儘管 compiler 在定義時沒有要求,但在實作給 !Sized 類型時也會出錯。
Note
所有 trait bound 預設都有 Sized,需要加上 ?Sized 放鬆限制讓 !Sized 類型也能實作。唯一的例外是宣告 trait 時,trait Trait {} 語法本身就是 trait Trait: ?Sized {}。