在 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?(stackoverflow.com/questions/5… ,这儿不再赘述。
常见问题
向上转型(upcast)
关于 trait SubTrait: Base
,在现在的 Rust 版别中,是无法将 &dyn SubTrait
转换到 &dyn Base
的。这个约束与 trait object 的内存结构有关。
在 Exploring Rust fat pointers(iandouglasscott.com/2018/05/28/… 一文中,该作者经过 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 之类指令),但是优点是更显着的:
- 能够为已有类型完成 trait(比方 blanket implementations)
- 调用虚表中的函数时,只需求引证一次,而在 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<T: Base> 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 特点。首要有以下几点:
- 函数返回类型不能是 Self(即当前类型)。这首要由于把一个目标转为 trait object 后,原始类型信息就丢掉了,所以这儿的 Self 也就无法确认了。
- 函数中不允许有泛型参数。首要原因在于单态化时会生成大量的函数,很简单导致 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 个完成
- 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 语言: www.infoq.cn/article/Uug…
- Abstraction without overhead: traits in Rust: blog.rust-lang.org/2015/05/11/…
- Advanced Traits: doc.rust-lang.org/book/ch19-0…
- Peeking inside Trait Objects: huonw.github.io/blog/2015/0…
- Object Safety: huonw.github.io/blog/2015/0…
- Interface Dispatch: lukasatkinson.de/2018/interf…
- 3 Things to Try When You Can’t Make a Trait Object: www.possiblerust.com/pattern/3-t…
本文最先发表于 RustMagazine 中文月刊rustmagazine.github.io/rust_magazi…
原文:liujiacai.net/blog/2021/0…