當代碼涉及多態(tài),需要有一種機制來確定實際上是哪些特定版本在運行。這就是所謂的“調度”。調度的主要形式有兩種:靜態(tài)調度和動態(tài)調度。Rust 不僅支持靜態(tài)調度,它還通過“特征對象”機制支持動態(tài)調度。
在本章的其余部分,我們將需要一個特征和一些實現(xiàn)。讓我們先從一個簡單的開始,F(xiàn)oo。它有一個方法,該方法將返回一個字符串。
trait Foo {
fn method(&self) -> String;
}
我們將在 u8 和 String 里面實現(xiàn)這個特征:
impl Foo for u8 {
fn method(&self) -> String { format!("u8: {}", *self) }
}
impl Foo for String {
fn method(&self) -> String { format!("string: {}", *self) }
}
我們可以利用這一特性的特征邊界來執(zhí)行靜態(tài)調度:
fn do_something<T: Foo>(x: T) {
x.method();
}
fn main() {
let x = 5u8;
let y = "Hello".to_string();
do_something(x);
do_something(y);
}
在這里 Rust 使用 “monomorphization” 來執(zhí)行靜態(tài)調度。這意味著 Rust 將為 u8 和 String 創(chuàng)建一個特殊版本的 do_something()函數(shù),然后調用這些具體的函數(shù)來取代調用 sites 。換句話說,Rust 生成是這樣的:
fn do_something_u8(x: u8) {
x.method();
}
fn do_something_string(x: String) {
x.method();
}
fn main() {
let x = 5u8;
let y = "Hello".to_string();
do_something_u8(x);
do_something_string(y);
}
這樣會有很大的好處:靜態(tài)調度允許內聯(lián)函數(shù)調用,因為被調用的函數(shù)在編譯時是已知的,并且內聯(lián)是進行良好的優(yōu)化的關鍵。靜態(tài)調度雖然快,但它是折衷之后的結果:代碼膨脹,由于許多相同的函數(shù)都有二進制副本,并且每種類型的函數(shù)都對應一個二進制副本。
另外,編譯器并不完美,可能“優(yōu)化”反而使代碼變得更慢。例如,內聯(lián)函數(shù)過于急切地將膨脹指令緩存(緩存規(guī)定我們周圍的一切)。這是 #[inline] 和 #[inline(always)] 應該被謹慎使用的部分原因,另一個使用動態(tài)調度的原因是它有時效率更高。
然而,常見的情況是使用靜態(tài)調度更高效,并且你總是可以用一個簡練的 statically-dispatched 包裝器函數(shù)進行動態(tài)調度,但是用動態(tài)調度卻不是這樣,這意味著靜態(tài)調用更靈活??赡芤驗檫@個原因,標準庫更多的使用靜態(tài)調度。
Rust 通過一個稱為特征對象的特性提供動態(tài)調度。特征對象,如 &Foo or Box<Foo>
可以是正常值,這個值存儲一個任意類型的值,而該類型實現(xiàn)了給定的特征,至于究竟是哪種類型在運行時才能知道。
特征對象可以從指針獲得,這個指針指向一個具體的類型,而該類型通過 cast(例如 &x as &Foo)或 coerce(例如,使用 &x 作為函數(shù)的參數(shù),函數(shù)里面帶有 &foo) 實現(xiàn)了特征。
特征對象 coercions 和 casts 也服務于指針像 &mut T to &mut Foo
與 Box<T> to Box<Foo>
但都只是在此刻有效。而 Coercions 和 casts 是相同的。
這個操作可以被視為“擦除”編譯器對特定類型的指針的認知,因此特征對象有時也被稱為類型擦除。
回到上面的例子,我們可以使用相同的特征來執(zhí)行動態(tài)調度與特征的對象的 cast:
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = 5u8;
do_something(&x as &Foo);
}
或者通過coerce::
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = "Hello".to_string();
do_something(&x);
}
每種實現(xiàn) Foo 的類型并不專門指定一個帶有個特征對象的函數(shù):只有當一個副本生成時,才會(但不總是如此)導致更少的代碼膨脹。然而,這是需要以更慢的虛擬函數(shù)調用作為代價的,并且極大地抑制了任何內聯(lián)的機會和相關優(yōu)化的發(fā)生。
在默認情況下,Rust 不給指針賦予初值,不像許多其他管理語言那樣,所以類型可以有不同的大小。在編譯時知道值的大小是很重要的,比如把它作為參數(shù)傳遞給一個函數(shù),又或者把它放進堆棧并且在存儲它的堆里面分配(或取消分配)空間給它。
對于 Foo,我們需要有一個值,這個值至少要是一個字符串(24 字節(jié))或 u8(1 個字節(jié)),或者是任意其他依賴于箱來實現(xiàn)他們的Foo的類型(任意的字節(jié)數(shù))。如果沒有存儲的值沒有指針來指向,就沒有辦法保證這最后一點行得通,因為其他那些類型可以是任意大小的。
讓指針指向一個值意味著,當我們拋出一個特征對象時,值的大小是無關緊要的,我們只關心指針本身的大小。
特征的方法可以通過特征對象來調用。特征對象是通過函數(shù)指針的一個特殊的記錄(由編譯器創(chuàng)建和管理)來調用方法的,該記錄傳統(tǒng)上也被稱為“虛表”。
特征對象既簡單的又復雜:其核心表示和布局其實很簡單,但也有一些復雜的錯誤消息和令人意外的結果。
讓我們以簡單的特征對象的運行時間的表示來開始。std::raw
模塊包含與內置類型一樣復雜的布局結構,包括特征對象:
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
即特征對象 &Foo 包含數(shù)據(jù)指針和一個虛表指針。
數(shù)據(jù)指針指向特征對象所存儲的數(shù)據(jù)(一些未知類型T),而虛表指針指向對應的虛表(“虛擬方法表”)對應于 T 的 Foo 實現(xiàn)。
vtable 本質上是一個函數(shù)指針結構體,指向每個實現(xiàn)方法的具體的機器代碼。調用方法 trait_object.method()
將檢索虛表外部的正確的指針,然后做一個動態(tài)調用。例如:
struct FooVtable {
destructor: fn(*mut ()),
size: usize,
align: usize,
method: fn(*const ()) -> String,
}
// u8:
fn call_method_on_u8(x: *const ()) -> String {
// the compiler guarantees that this function is only called
// with `x` pointing to a u8
let byte: &u8 = unsafe { &*(x as *const u8) };
byte.method()
}
static Foo_for_u8_vtable: FooVtable = FooVtable {
destructor: /* compiler magic */,
size: 1,
align: 1,
// cast to a function pointer
method: call_method_on_u8 as fn(*const ()) -> String,
};
// String:
fn call_method_on_String(x: *const ()) -> String {
// the compiler guarantees that this function is only called
// with `x` pointing to a String
let string: &String = unsafe { &*(x as *const String) };
string.method()
}
static Foo_for_String_vtable: FooVtable = FooVtable {
destructor: /* compiler magic */,
// values for a 64-bit computer, halve them for 32-bit ones
size: 24,
align: 8,
method: call_method_on_String as fn(*const ()) -> String,
};
每個 vtable 的析構函數(shù)的字段都指向一個函數(shù),這個函數(shù)將清理 vtable 類型的任何資源,對 u8 來說這幾乎是沒有什么用處的,但對字符串來說卻可以釋放內存。如果想要擁有 Box<Foo>
那樣的特征對象,這個操作就是必不可少的,當超出范圍時,就需要清理 Box 分配以及內部類型。size 和 align 字段存儲著被刪除類型的大小,及其一致性要求;由于信息被嵌入到了析構函數(shù),目前這些實際上都未使用,但將來會得到應用,因為特征對象逐漸變得更加靈活。
假設我們有一些實現(xiàn) Foo 的值,然后你會發(fā)現(xiàn) Foo 的顯式形式的構建和 Foo 特征對象的使用看起來有點類似(忽略類型不匹配:無論如何,它們只是指針而已):
let a: String = "foo".to_string();
let x: u8 = 1;
// let b: &Foo = &a;
let b = TraitObject {
// store the data
data: &a,
// store the methods
vtable: &Foo_for_String_vtable
};
// let y: &Foo = x;
let y = TraitObject {
// store the data
data: &x,
// store the methods
vtable: &Foo_for_u8_vtable
};
// b.method();
(b.vtable.method)(b.data);
// y.method();
(y.vtable.method)(y.data);
如果 b 或 y 擁有特征對象 (Box < Foo >
) 當他們超出范圍時,就會有一個(b.vtable.destructor
)(b.data) (分別 y)調用。
更多建議: