前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >trait 对象的静态分发与动态分发

trait 对象的静态分发与动态分发

作者头像
charmer
发布2024-05-15 21:00:58
700
发布2024-05-15 21:00:58
举报
文章被收录于专栏:编程大主教编程大主教

rust by example 是这么定义 trait 的 [1]

Traits

A trait is a collection of methods defined for an unknown type: Self. They can access other methods declared in the same trait.

自然,我们就会需要传递“实现了某个 trait”的 struct 这种范型能力。在 rust 中,提供了 两种方式 来实现这种能力,先引入一个 trait 和两个 struct 用于讲解后面的内容。

代码语言:javascript
复制
trait Run {
    fn run(&self);
}

struct Duck;

impl Run for Duck {
    fn run(&self) {
        println!("Duck is running");
    }
}

struct Dog;

impl Run for Dog {
    fn run(&self) {
        println!("Dog is running");
    }
}

静态分发和动态分发

首先引入分发 (dispatch):当代码涉及多态时,编译器需要某种机制去决定实际的调用关系。rust 提供了两种分发机制,分别是静态分发 (static dispatch) 和动态分发 (dynamic dispatch)。[2]

静态分发

静态分发其实就是编译期范型,所有静态分发在编译期间确定实际类型,Rustc 会通过单态化 (Monomorphization) 将泛型函数展开。

而静态分发有两种形式:

代码语言:javascript
复制
fn get_runnable<T>(runnable: T) where T: Run {
    runnable.run();
}

fn get_runnable(runnable: impl Run) {
    runnable.run();
}

两者在调用时都能通过

代码语言:javascript
复制
get_runnable(Dog {});

方式调用,区别在于前者可以使用 turbo-fish 语法(也就是 ::<> 操作符):

代码语言:javascript
复制
get_runnable::<Dog>(Dog {});

动态分发

首先引入 trait对象(trait object) 的概念:trait 对象是指实现了某组 traits 的非具体类型值,这组 trait 一定包含一个 对象安全(object safe) 的基 trait,和一些 自动trait(auto trait)

在 2021 版本后,要求 trait 对象一定需要 dyn 关键字标识,以和 trait 本身区分开来。对于某个 trait MyTrait,以下东西都是 trait 对象 [3]

  • dyn MyTrait
  • dyn MyTrait + Send
  • dyn MyTrait + Send + Sync
  • dyn MyTrait + 'static
  • dyn MyTrait + Send + 'static
  • dyn MyTrait +
  • dyn 'static + MyTrait
  • dyn (MyTrait)

动态分发也就是运行时范型,虽然 trait 对象是 Dynamically Sized Types(DST, 也叫unsized types),意味着它的大小只有运行时可以确定,意味着 rustc 不会允许这样的代码通过编译:

代码语言:javascript
复制
fn get_runnable(runnable: dyn Run) {
    runnable.run();
}

但是指向实现 trait 的 struct 的指针大小是一定的,因此可以把 trait 对象隐藏在指针后(如 &dyn TraitBox<dyn Trait>Rc<dyn Trait> 等),编译器编译时会默认对象实现了 trait,并在运行时动态加载调用的对应函数。

代码语言:javascript
复制
fn get_runnable(runnable: &dyn Run) {
    runnable.run();
}

动态分发靠的就是指向 trait 对象的指针。

实现原理

静态分发

静态分发的实现原理比较简单,每多一种调用类型,rustc 就会生成多一个函数:

代码语言:javascript
复制
fn get_runnable<T>(runnable: T) where T: Run {
    runnable.run();
}

fn main() {
    get_runnable::<Dog>(Dog {});
    get_runnable::<Duck>(Duck {});
}

通过编译后,get_runnable 函数会生成两种:

代码语言:javascript
复制
fn get_runnable_for_dog(runnable: Dog) {
    runnable.run()
}

fn get_runnable_for_duck(runnable: Duck) {
    runnable.run()
}

rustc 会自动将类型与调用函数匹配。

显而易见的,通过静态分发实现的多态无运行时性能损耗,但是编译出的二进制文件大小增加。

动态分发

动态分发就略复杂了,实现的关键在指针,每个指向 trait 对象的指针包含:

  • 指向实现某个 trait 实例的指针
  • 虚拟函数列表 (virtual method table, 一般直接叫 vtable),包含
    • 某个 trait 和它父 trait 的所有函数
    • 指向这个实例对函数列表内函数的实现的指针

使用 trait 对象的目的是对方法的“延迟绑定(late binding)”,调用 trait 对象的某个方法最终在运行时才分发,也就是说调用时先从 vtable 中载入函数指针,再间接调用这个函数。对于 vtable 中每一个函数的实现,每个 trait 对象都可以不一样。

其实 rust 中 str 字符串类型和 [T] 数组类型都是 trait 对象。

对象安全

trait 对象一定要基于 对象安全 的 trait,这里不大谈特谈,只简单提及两个有趣的地方。

std::Sized

  • 当不希望 trait 被用为 trait 对象时,可以加上 Self: Sized 的约束
  • 当不希望某个函数出现在 trait 对象的 vtable 中,可以加上 where Self: Sized 的约束

trait 对象的可分发函数不能有类型(范型)参数,所以如果 trait 中存在范型参数,只能静态分发了

代码语言:javascript
复制
trait Run {
 fn run<T>(&self, t: T);
}

Self 只能出现在方法的接受者(receiver)中,也就是方法的第一个参数,&self&mut self...

  1. https://doc.rust-lang.org/rust-by-example/trait.html ↩︎
  2. https://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/trait-objects.html ↩︎
  3. https://doc.rust-lang.org/reference/types/trait-object.html ↩︎
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 静态分发和动态分发
    • 静态分发
      • 动态分发
      • 实现原理
        • 静态分发
          • 动态分发
          • 对象安全
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档