通用特质有界方法与“直接”特质方法的区别

时间:2019-11-21 16:22:30

标签: rust

我有此代码:

fn main() {
    let p = Person;
    let r = &p as &dyn Eatable;

    Consumer::consume(r);
    // Compile error
    Consumer::consume_generic(r);
}

trait Eatable {}

struct Person;

impl Eatable for Person {}

struct Consumer;

impl Consumer {
    fn consume(eatable: &dyn Eatable) {}
    fn consume_generic<T: Eatable>(eatable: &T) {}
}

错误:

  

类型为dyn Eatable的值的大小在   编译时间

我认为这很奇怪。我有一个方法,该方法实际上可以使用dyn Eatable并可以正常编译,因此该方法可以某种方式知道Eatable的大小。通用方法(consume_generic将针对每种使用的类型正确编译以提高性能,而consume方法则不会。

因此出现了几个问题:为什么编译器出错? 方法内部是否存在可以执行其他方法无法完成的事情?何时应该优先使用另一种方法?

旁注:我也用Swift语言问了这个问题:Differences generic protocol type parameter vs direct protocol type。在Swift中,我得到相同的编译错误,但是潜在的错误却不同:协议/特征不符合自身(因为Swift协议可以容纳初始化程序,静态内容等,这使得更难以通用地引用它们)。我也在Java中尝试过,我相信通用类型会被删除,并且绝对没有区别。

1 个答案:

答案 0 :(得分:1)

问题不在于函数本身,而在于类型的特征范围。

Rust中的每个泛型类型都有一个隐式的Sized界限:由于在大多数情况下这是正确的,因此决定不强迫开发人员每次都将其写出来。但是,如果仅在某种类型的引用后面使用此类型,如此处所述,则可能需要通过指定T: ?Sized来取消此限制。如果添加此代码,则代码可以正常编译:

impl Consumer {
    fn consume(eatable: &dyn Eatable) {}
    fn consume_generic<T: Eatable + ?Sized>(eatable: &T) {}
}

Playground as a proof


对于其他问题,主要区别在于静态调度与动态调度。

当您使用通用函数(或语义等效的impl Trait语法)时,该函数调用是静态分派的。也就是说,对于传递给函数的每种参数类型,编译器都会独立于其他参数生成定义。在大多数情况下,这可能会导致代码更优化,但缺点可能是二进制文件更大,并且API受到某些限制(例如,您无法轻松地以这种方式创建异构集合)。

使用dyn Trait语法时,您选择加入动态分派。必要的数据将存储在附加到trait对象的表中,并且将在运行时为每个trait方法选择正确的实现。但是,使用者只需要编译一次。通常这是较慢的,这既是由于间接的原因,又是因为不可能进行单独的优化,但更灵活。


至于建议(请注意,这只是一种观点,而不是事实)-我想说,最好尽可能地坚持使用泛型,并且只有在否则无法实现目标的情况下,才将其更改为特征对象。