Rust 中 Trait 的使用及实现分析
尘香 蚂蚁智能监控 今天

使用方式基本用法静态派发动态派发impl trait高阶用法关联类型Derive常见问题向上转型(upcast)向下转型(downcast)Object safety总结参考

在 Rust 设计目标中,零成本抽象是非常重要的一条,它让 Rust 具备高级语言表达能力的同时,又不会带来性能损耗。零成本的基石是泛型与 trait,它们可以在编译期把高级语法编译成与高效的底层代码,从而实现运行时的高效。这篇文章就来介绍 trait,包括使用方式与三个常见问题的分析,在问题探究的过程中来阐述其实现原理。

本文最先发表于 RustMagazine 中文月刊(https://rustmagazine.github.io/rust_magazine_2021/chapter_4/ant_trait.html)

使用方式
基本用法

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 等。

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 objectlet 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 abovething.foo(1);thing.foo("hello");
}
// 总共会有 10 * 3 = 30 个实现
Trait 不能继承 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_eIJusEka1aSPmQMAbstraction without overhead: traits in Rust: https://blog.rust-lang.org/2015/05/11/traits.htmlAdvanced Traits: https://doc.rust-lang.org/book/ch19-03-advanced-traits.htmlPeeking 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

关于我们

我们是蚂蚁智能监控技术中台的时序存储团队,我们正在使用 Rust 构建高性能、低成本并具备实时分析能力的新一代时序数据库,欢迎加入或者推荐,目前我们也正在寻找优秀的实习生,也欢迎广大应届同学来我们团队实习,请联系:jiachun.fjc@antgroup.com

Rust 中 Trait 的使用及实现分析相关推荐

  1. rust为什么显示不了国服_捋捋 Rust 中的 impl Trait 和 dyn Trait

    缘起 一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突然想写一个可以浏览和背诵诗词的 TUI 程序说起. 我选择了 Cursive 这个 Rust TUI 库. 在实现时有这么一个函 ...

  2. 【Rust投稿】捋捋 Rust 中的 impl Trait 和 dyn Trait

    本文来自 PrivateRookie 的知乎投稿:https://zhuanlan.zhihu.com/p/109990547 缘起 一切都要从年末换工作碰上特殊时期, 在家闲着无聊又读了几首诗, 突 ...

  3. rust腐蚀深井_深井开采中的地压现象致因分析及措施_高光

    Gold Science and Technology Vol . 1 4, No. 4 J u l . 2 0 0 6 深井开采中的地压现象致因分析及措施 * 高 光 ( 中国黄金集团二道沟金矿 , ...

  4. 10玩rust_有趣的 Rust 类型系统: Trait

    也许你已经学习了标准库提供的 String 类型,这是一个 UTF-8 编码的可增长字符串.该类型的结构为: pub struct String {vec: Vec<u8>, } UTF- ...

  5. Rust 中的继承与代码复用

    Rust 中的继承与代码复用 在学习Rust过程中突然想到怎么实现继承,特别是用于代码复用的继承,于是在网上查了查,发现不是那么简单的. C++的继承 首先看看c++中是如何做的. 例如要做一个场景结 ...

  6. Draconian,自由或保姆状态:Java,C#,C,C ++,Go和Rust中的并发意识形态

    为什么我们需要并发 (Why we need Concurrency) Once, there was a good old time when clock speed doubled every 1 ...

  7. rust笔记7 rust中的包管理

    rust相比于C++,一个优势在于有一个现代化的包管理系统,我们不用搞各种命令空间和依赖的问题.这里主要记录了一般文件打包的方式. rust中声明包的关键字是mod,如果是公共的,则需要声明为pub ...

  8. 17.Rust中函数式语言功能:迭代器与闭包

    Rust 的设计灵感来源于很多现存的语言和技术.其中一个显著的影响就是 函数式编程(functional programming).函数式编程风格通常包含将函数作为参数值或其他函数的返回值.将函数赋值 ...

  9. Rust 常用 trait 实现

    1. Eq & PartialEq 符号:==.!= 区别:Eq 相比于 PartialEq 还需额外满足反身性,即 a == a.对于浮点类型,Rust 只实现了 PartialEq 而不是 ...

  10. i3够晚rust吗_【译】理解Rust中的Futures (一)

    原文标题:Understanding Futures In Rust -- Part 1 原文链接:https://www.viget.com/articles/understanding-futur ...

最新文章

  1. 用计算机计算教学反思,《用计算器计算》教学反思
  2. Servlet、Tomcat、 SpringMVC 之间的关系
  3. c#二叉树 取叶子节点个数_二叉树的最小深度+完全二叉树的节点个数
  4. .NET Core 3.0 使用Nswag生成Api文档和客户端代码
  5. 【转】日邮物流:实现智慧物流,这个云上对了!
  6. mysql 第二天数据_MySQL入门第二天------数据库操作
  7. ubuntu软件安装 caffe相关软件安装 学习笔记
  8. [WSE]如何启用WSE2.0的强大的Trace功能
  9. Python开发环境Linux配置
  10. 【收藏】Windows 8 Consumer Preview的108个运行命令及简要说明
  11. 越狱iphone安装mysql,CentOS 7 基于DCRM搭建自有Cydia越狱源
  12. java源码app,飞飞CMS双端JAVA原生APP源码
  13. 微信小程序开发教程——1.0.1appid注册和开发者工具下载
  14. 大火的何铠明:MAE——用于计算机视觉的可扩展自监督学习神器
  15. 隧道测量快速坐标反程序48004850计算器
  16. matlab是计算机模拟吗,MATLAB计算机模拟,MATLAB calculator simulate,音标,读音,翻译,英文例句,英语词典...
  17. 永磁材料介绍和ANSYS Workbench永磁体仿真
  18. 分享10大自动化测试框架,你用过几个?
  19. 备份Linux到ntfs硬盘,Linux(SLES)挂载NTFS移动硬盘实践
  20. 百度地图JavaScript API 学习之地址解析

热门文章

  1. 阿里巴巴正式开源全球化OpenMessaging和ApsaraCache项目
  2. Android业务组件化之Gradle和Sonatype Nexus搭建私有maven仓库
  3. 开源中国 OsChina Android 客户端源码分析(12)清理缓存
  4. 这个世界是怎么了?做商业软件的怎么越来越流氓了?
  5. Java-集合第四篇Queue集合
  6. loj #6122. 「网络流 24 题」航空路线问题
  7. 移动端 短信发送,一键拨号功能
  8. 分布式系统常用思想和技术
  9. 改造u3d第一人称控制器,使之适合Cardboard+蓝牙手柄控制
  10. CentOS-6.3安装配置Tomcat-7