Rust 中 Trait 的使用及實現分析

語言: CN / TW / HK

在 Rust 設計目標中,零成本抽象是非常重要的一條,它讓 Rust 具備高級語言表達能力的同時,又不會帶來性能損耗。零成本的基石是泛型與 trait,它們可以在編譯期把高級語法編譯成與高效的底層代碼,從而實現運行時的高效。這篇文章就來介紹 trait,包括使用方式與三個常見問題的分析,在問題探究的過程中來闡述其實現原理。

使用方式

基本用法

Trait 的主要作用是用來抽象行為,類似於其他編程語言中的「接口」,這裏舉一示例闡述 trait 的基本使用方式:

trait Greeting { fn greeting(&self) -> &str; } struct Cat; impl Greeting for Cat { fn greeting(&self) -> &str { "Meow!" } } struct Dog; impl Greeting for Dog { fn greeting(&self) -> &str { "Woof!" } }

在上述代碼中,定義了一個 trait Greeting,兩個 struct 實現了它,根據函數調用方式,主要兩種使用方式:

  • 基於泛型的靜態派發
  • 基於 trait object 的動態派發

泛型的概念比較常見,這裏着重介紹下 trait object:

A trait object is an opaque value of another type that implements a set of traits. The set of traits is made up of an object safe base trait plus any number of auto traits.

比較重要的一點是 trait object 屬於 Dynamically Sized Types(DST),在編譯期無法確定大小,只能通過指針來間接訪問,常見的形式有 Box<dyn trait> &dyn trait 等。

fn print_greeting_static<G: Greeting>(g: G) { println!("{}", g.greeting()); } fn print_greeting_dynamic(g: Box<dyn Greeting>) { println!("{}", g.greeting()); } print_greeting_static(Cat); print_greeting_static(Dog); print_greeting_dynamic(Box::new(Cat)); print_greeting_dynamic(Box::new(Dog));

靜態派發

在 Rust 中,泛型的實現採用的是單態化(monomorphization),會針對不同類型的調用者,在編譯時生成不同版本的函數,所以泛型也被稱為類型參數。好處是沒有虛函數調用的開銷,缺點是最終的二進制文件膨脹。在上面的例子中, print_greeting_static 會編譯成下面這兩個版本:

print_greeting_static_cat(Cat); print_greeting_static_dog(Dog);

動態派發

不是所有函數的調用都能在編譯期確定調用者類型,一個常見的場景是 GUI 編程中事件響應的 callback,一般來説一個事件可能對應多個 callback 函數,而這些 callback 函數都是在編譯期不確定的,因此泛型在這裏就不適用了,需要採用動態派發的方式:

trait ClickCallback { fn on_click(&self, x: i64, y: i64); } struct Button { listeners: Vec<Box<dyn ClickCallback>>, }

impl trait

在 Rust 1.26 版本中,引入了一種新的 trait 使用方式,即:impl trait,可以用在兩個地方:函數參數與返回值。該方式主要是簡化複雜 trait 的使用,算是泛型的特例版,因為在使用 impl trait 的地方,也是靜態派發,而且作為函數返回值時,數據類型只能有一種,這一點要尤為注意!

fn print_greeting_impl(g: impl Greeting) { println!("{}", g.greeting()); } print_greeting_impl(Cat); print_greeting_impl(Dog); // 下面代碼會編譯報錯 fn return_greeting_impl(i: i32) -> impl Greeting { if i > 10 { return Cat; } Dog } // | fn return_greeting_impl(i: i32) -> impl Greeting { // | ------------- expected because this return type... // | if i > 10 { // | return Cat; // | --- ...is found to be `Cat` here // | } // | Dog // | ^^^ expected struct `Cat`, found struct `Dog`

高階用法

關聯類型

在上面介紹的基本用法中,trait 中方法的參數或返回值類型都是確定的,Rust 提供了類型「惰性綁定」的機制,即關聯類型(associated type),這樣就能在實現 trait 時再來確定類型,一個常見的例子是標準庫中的 Iterator,next 的返回值為 Self::Item

trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } /// 一個只輸出偶數的示例 struct EvenNumbers { count: usize, limit: usize, } impl Iterator for EvenNumbers { type Item = usize; fn next(&mut self) -> Option<Self::Item> { if self.count > self.limit { return None; } let ret = self.count * 2; self.count += 1; Some(ret) } } fn main() { let nums = EvenNumbers { count: 1, limit: 5 }; for n in nums { println!("{}", n); } } // 依次輸出 2 4 6 8 10

關聯類型的使用和泛型相似,Iterator 也可使用泛型來定義:

pub trait Iterator<T> { fn next(&mut self) -> Option<T>; }

它們的區別主要在於:

  • 一個特定類型(比如上文中的 Cat)可以多次實現泛型 trait。比如對於 From,可以有 impl From<&str> for Cat 也可以有 impl From<String> for Cat
  • 但是對於關聯類型的 trait,只能實現一次。比如對於 FromStr,只能有 impl FromStr for Cat ,類似的 trait 還有 Iterator Deref

Derive 宏

在 Rust 中,可以使用 derive 屬性來實現一些常用的 trait,比如:Debug/Clone 等,對於用户自定義的 trait,也可以實現過程宏支持 derive,具體可參考:How to write a custom derive macro?(https://stackoverflow.com/questions/53135923/how-to-write-a-custom-derive-macro/53136446#53136446) ,這裏不再贅述。

常見問題

向上轉型(upcast)

對於 trait SubTrait: Base ,在目前的 Rust 版本中,是無法將 &dyn SubTrait 轉換到 &dyn Base的。這個限制與 trait object 的內存結構有關。

在 Exploring Rust fat pointers(https://iandouglasscott.com/2018/05/28/exploring-rust-fat-pointers/) 一文中,該作者通過 transmute 將 trait object 的引用轉為兩個 usize,並且驗證它們是指向數據與函數虛表的指針:

use std::mem::transmute; use std::fmt::Debug; fn main() { let v = vec![1, 2, 3, 4]; let a: &Vec<u64> = &v; // 轉為 trait object let b: &dyn Debug = &v; println!("a: {}", a as *const _ as usize); println!("b: {:?}", unsafe { transmute::<_, (usize, usize)>(b) }); } // a: 140735227204568 // b: (140735227204568, 94484672107880)

從這裏可以看出:Rust 使用 fat pointer(即兩個指針) 來表示 trait object 的引用,分佈指向 data 與 vtable,這和 Go 中的 interface 十分類似。

pub struct TraitObjectReference { pub data: *mut (), pub vtable: *mut (), } struct Vtable { destructor: fn(*mut ()), size: usize, align: usize, method: fn(*const ()) -> String, }

儘管 fat pointer 導致指針體積變大(無法使用 Atomic 之類指令),但是好處是更明顯的:

  1. 可以為已有類型實現 trait(比如 blanket implementations)
  2. 調用虛表中的函數時,只需要引用一次,而在 C++ 中,vtable 是存在對象內部的,導致每一次函數調用都需要兩次引用,如下圖所示:

如果 trait 有繼承關係時,vtable 是怎麼存儲不同 trait 的方法的呢?在目前的實現中,是依次存放在一個 vtable 中的,如下圖:

可以看到,所有 trait 的方法是順序放在一起,並沒有區分方法屬於哪個 trait,這樣也就導致無法進行 upcast,社區內有 RFC 2765 在追蹤這個問題,感興趣的讀者可參考,這裏就不討論解決方案了,介紹一種比較通用的解決方案,通過引入一個 AsBase 的 trait 來解決:

``` trait Base { fn base(&self) { println!("base..."); } } trait AsBase { fn as_base(&self) -> &dyn Base; } // blanket implementation impl AsBase for T { fn as_base(&self) -> &dyn Base { self } } trait Foo: AsBase { fn foo(&self) { println!("foo.."); } }

[derive(Debug)]

struct MyStruct; impl Foo for MyStruct {} impl Base for MyStruct {} fn main() { let s = MyStruct; let foo: &dyn Foo = &s; foo.foo(); let base: &dyn Base = foo.as_base(); base.base(); } ```

向下轉型(downcast)

向下轉型是指把一個 trait object 再轉為之前的具體類型,Rust 提供了 Any 這個 trait 來實現這個功能。

pub trait Any: 'static { fn type_id(&self) -> TypeId; }

大多數類型都實現了 Any,只有那些包含非 'static 引用的類型沒有實現。通過 type_id 就能夠在運行時判斷類型,下面看一示例:

use std::any::Any; trait Greeting { fn greeting(&self) -> &str; fn as_any(&self) -> &dyn Any; } struct Cat; impl Greeting for Cat { fn greeting(&self) -> &str { "Meow!" } fn as_any(&self) -> &dyn Any { self } } fn main() { let cat = Cat; let g: &dyn Greeting = &cat; println!("greeting {}", g.greeting()); // &Cat 類型 let downcast_cat = g.as_any().downcast_ref::<Cat>().unwrap(); println!("greeting {}", downcast_cat.greeting()); }

上面的代碼重點在 downcast_ref,其實現為:

pub fn downcast_ref<T: Any>(&self) -> Option<&T> { if self.is::<T>() { unsafe { Some(&*(self as *const dyn Any as *const T)) } } else { None } }

可以看到,在類型一致時,通過 unsafe 代碼把 trait object 引用的第一個指針(即 data 指針)轉為了指向具體類型的引用。

Object safety

在 Rust 中,並不是所有的 trait 都可用作 trait object,需要滿足一定的條件,稱之為 object safety 屬性。主要有以下幾點:

  1. 函數返回類型不能是 Self(即當前類型)。這主要因為把一個對象轉為 trait object 後,原始類型信息就丟失了,所以這裏的 Self 也就無法確定了。
  2. 函數中不允許有泛型參數。主要原因在於單態化時會生成大量的函數,很容易導致 trait 內的方法膨脹。比如

trait Trait { fn foo<T>(&self, on: T); // more methods } // 10 implementations fn call_foo(thing: Box<Trait>) { thing.foo(true); // this could be any one of the 10 types above thing.foo(1); thing.foo("hello"); } // 總共會有 10 * 3 = 30 個實現

  1. Trait 不能"繼承(trait bound)" Sized。這是由於 Rust 會默認為 trait object 實現該 trait,生成類似下面的代碼: 如果 Foo 繼承了 Sized,那麼就要求 trait object 也是 Sized,而 trait object 是 DST 類型,屬於 ?Sized ,所以 trait 不能繼承 Sized。 對於非 safe 的 trait,能修改成 safe 是最好的方案,如果不能,可以嘗試泛型的方式。

trait Foo { fn method1(&self); fn method2(&mut self, x: i32, y: String) -> usize; } // autogenerated impl impl Foo for TraitObject { fn method1(&self) { // `self` is an `&Foo` trait object. // load the right function pointer and call it with the opaque data pointer (self.vtable.method1)(self.data) } fn method2(&mut self, x: i32, y: String) -> usize { // `self` is an `&mut Foo` trait object // as above, passing along the other arguments (self.vtable.method2)(self.data, x, y) } }

總結

本文開篇就介紹了 trait 是實現零成本抽象的基礎,通過 trait 可以為已有類型增加新方法,這其實解決了表達式問題,可以進行運算符重載,可以進行面向接口編程等。希望通過本文的分析,可以讓讀者更好的駕馭 trait 的使用,在面對編譯器錯誤時,能夠做到遊刃有餘。

參考

  • 想要改變世界的 Rust 語言: https://www.infoq.cn/article/Uugi_eIJusEka1aSPmQM
  • Abstraction without overhead: traits in Rust: https://blog.rust-lang.org/2015/05/11/traits.html
  • Advanced Traits: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
  • Peeking inside Trait Objects: http://huonw.github.io/blog/2015/01/peeking-inside-trait-objects/
  • Object Safety: http://huonw.github.io/blog/2015/01/object-safety/
  • Interface Dispatch: https://lukasatkinson.de/2018/interface-dispatch/
  • 3 Things to Try When You Can't Make a Trait Object: https://www.possiblerust.com/pattern/3-things-to-try-when-you-can-t-make-a-trait-object

本文最先發表於 RustMagazine 中文月刊https://rustmagazine.github.io/rust_magazine_2021/chapter_4/ant_trait.html

原文:https://liujiacai.net/blog/2021/04/27/trait-usage/