特征的對象

2018-08-12 22:03 更新

特征的對象

當代碼涉及多態(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) }
}

靜態(tài)調度

我們可以利用這一特性的特征邊界來執(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)調度。

動態(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 FooBox<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)調用。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號