當涉及到多態的代碼時,我們需要一個機制來決定哪個具體的版本應該得到執行。這叫做“分發”(dispatch)。大體上有兩種形式的分發:靜態分發和動態分發。雖然Rust喜歡靜態分發,不過它也提供了一個叫做“trait對象”的機制來支持動態分發。
## 背景
在本章接下來的內容中,我們需要一個trait和一些實現。讓我們來創建一個簡單的`Foo`。它有一個返回`String`的方法。
~~~
trait Foo {
fn method(&self) -> String;
}
~~~
我們也在`u8`和`String`上實現了這個特性:
~~~
impl Foo for u8 {
fn method(&self) -> String { format!("u8: {}", *self) }
}
impl Foo for String {
fn method(&self) -> String { format!("string: {}", *self) }
}
~~~
## 靜態分發
我們可以使用trait的限制來進行靜態分發:
~~~
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用“單態”來進行靜態分發。這意味著Rust會為`u8`和`String`分別創建一個特殊版本的的`do_something()`,然后將對`do_something`的調用替換為這些特殊函數。也就是說,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);
}
~~~
這有一個很牛的好處:靜態分發允許函數被內聯調用,因為調用者在編譯時就知道它,內聯對編譯器進行代碼優化十分有利。靜態分發能提高程序的運行效率,不過相應的也有它的弊端:會導致“代碼膨脹”(code bloat)。因為在編譯出的二進制程序中,同樣的函數,對于每個類型都會有不同的拷貝存在。
此外,編譯器也不是完美的并且“優化”后的代碼可能更慢。例如,過度的函數內聯會導致指令緩存膨脹(緩存控制著我們周圍的一切)。這也是為何要謹慎使用`#[inline]`和`#[inline(always)]`的部分原因。另外一個使用動態分發的原因是,在一些情況下,動態分發更有效率。
然而,常規情況下靜態分發更有效率,并且我們總是可以寫一個小的靜態分發的封裝函數來進行動態分發,不過反過來不行,這就是說靜態調用更加靈活。因為這個原因標準庫盡可能的使用了靜態分發。
## 動態分發
Rust通過一個叫做“trait對象”的功能提供動態分發。比如說`&Foo`、`Box`這些就是trait對象。它們是一些值,值中儲存實現了特定trait的_任意_類型。它的具體類型只能在運行時才能確定。
從一些實現了特定`trait`的類型的指針中,可以從通過_轉型_(casting)(例如,`&x as &Foo`)或者_強制轉型_(coercing it)(例如,把`&x`當做參數傳遞給一個接收`&Foo`類型的函數)來取得trait對象。
這些trait對象的強制多態和轉型也適用于類似于`&mut Foo`的`&mut T`以及`Box`的`Box`這樣的指針,也就是目前為止我們討論到的所有指針。強制轉型和轉型是一樣的。
這個操作可以被看作“清除”編譯器關于特定類型指針的信息,因此trait對象有時被稱為“類型清除”(type erasure)。
回到上面的例子,我們可以使用相同的trait,通過trait對象的轉型(casting)來進行動態分發:
~~~
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = 5u8;
do_something(&x as &Foo);
}
~~~
或者通過強制轉型(by concercing):
~~~
fn do_something(x: &Foo) {
x.method();
}
fn main() {
let x = "Hello".to_string();
do_something(&x);
}
~~~
一個使用trait對象的函數并沒有為每個實現了`Foo`的類型專門生成函數:它只有一份函數的代碼,一般(但不總是)會減少代碼膨脹。然而,因為調用虛函數,會帶來更大的運行時開銷,也會大大地阻止任何內聯以及相關優化的進行。
### 為什用指針?
和很多托管語言不一樣,Rust默認不用指針來存放數據,因此類型有著不同的大小。在編譯時知道值的大小(size),以及了解把值作為參數傳遞給函數、值在棧上移動、值在堆上分配(或釋放)并儲存等情況,對于Rust程序員來說是很重要的。
對于`Foo`,我們需要一個值至少是一個`String`(24字節)或一個`u8`(1字節),或者其它crate中可能實現了`Foo`(任意字節)的其他類型。如果值沒有使用指針存儲,我們無法保證代碼能對其他類型正常運作,因為其它類型可以是任意大小的。
用指針來儲存值意味著當我們使用trait對象時值的大小(size)是無關的,只與指針的大小(size)有關。
### 表現(Representation)
可以在一個trait對象上通過一個特殊的函數指針的記錄調用的特性函數通常叫做“虛函數表”(由編譯器創建和管理)。
trait對象既簡單又復雜:它的核心表現和設計是十分直觀的,不過這有一些難懂的錯誤信息和詭異行為有待發掘。
讓我們從一個簡單的,帶有trait對象的運行時表現開始。`std::raw`模塊包含與復雜的內建類型有相同結構的結構體,[包括trait對象](http://doc.rust-lang.org/std/raw/struct.TraitObject.html):
~~~
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (),
}
~~~
這就是了,一個trait對象就像包含一個“數據”指針和“虛函數表”指針的`&Foo`。
數據指針指向trait對象保存的數據(某個未知的類型`T`),和一個虛表指針指向對應`T`的`Foo`實現的虛函數表。
一個虛表本質上是一個函數指針的結構體,指向每個函數實現的具體機器碼。一個像`trait_object.method()`的函數調用會從虛表中取出正確的指針然后進行一個動態調用。例如:
~~~
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,
};
~~~
在每個虛表中的`destructor`字段指向一個會清理虛表類型的任何資源的函數,對于`u8`是普通的,不過對于`String`它會釋放內存。這對于像`Box`這類有所有權的trait對象來說是必要的,它需要在離開作用域后清理`Box`分配和他內部的類型。`size`和`align`字段儲存需要清除類型的大小和它的對齊情況;它們原理上是無用的因為這些信息已經嵌入了析構函數中,不過在將來會被使用到,因為trait對象正日益變得更靈活。
假設我們有一些實現了`Foo`的值,那么顯式的創建和使用`Foo`trait對象可能看起來有點像這個(忽略不匹配的類型,它們只是指針而已):
~~~
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`擁有trait對象(`Box`),在它們離開作用域后會有一個`(b.vtable.destructor)(b.data)`(相應的還有`y`的)調用。
- 前言
- 1.介紹
- 2.準備
- 2.1.安裝Rust
- 2.2.Hello, world!
- 2.3.Hello, Cargo!
- 3.學習Rust
- 3.1.猜猜看
- 3.2.哲學家就餐問題
- 3.3.其它語言中的Rust
- 4.高效Rust
- 4.1.棧和堆
- 4.2.測試
- 4.3.條件編譯
- 4.4.文檔
- 4.5.迭代器
- 4.6.并發
- 4.7.錯誤處理
- 4.8.外部語言接口
- 4.9.Borrow 和 AsRef
- 4.10.發布途徑
- 5.語法和語義
- 5.1.變量綁定
- 5.2.函數
- 5.3.原生類型
- 5.4.注釋
- 5.5.If語句
- 5.6.for循環
- 5.7.while循環
- 5.8.所有權
- 5.9.引用和借用
- 5.10.生命周期
- 5.11.可變性
- 5.12.結構體
- 5.13.枚舉
- 5.14.匹配
- 5.15.模式
- 5.16.方法語法
- 5.17.Vectors
- 5.18.字符串
- 5.19.泛型
- 5.20.Traits
- 5.21.Drop
- 5.22.if let
- 5.23.trait對象
- 5.24.閉包
- 5.25.通用函數調用語法
- 5.26.包裝箱和模塊
- 5.27.`const`和`static`
- 5.28.屬性
- 5.29.`type`別名
- 5.30.類型轉換
- 5.31.關聯類型
- 5.32.不定長類型
- 5.33.運算符和重載
- 5.34.`Deref`強制多態
- 5.35.宏
- 5.36.裸指針
- 6.Rust開發版
- 6.1.編譯器插件
- 6.2.內聯匯編
- 6.3.不使用標準庫
- 6.4.固有功能
- 6.5.語言項
- 6.6.鏈接參數
- 6.7.基準測試
- 6.8.裝箱語法和模式
- 6.9.切片模式
- 6.10.關聯常量
- 7.詞匯表
- 8.學院派研究
- 勘誤