我有此代码:
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中尝试过,我相信通用类型会被删除,并且绝对没有区别。
答案 0 :(得分:1)
问题不在于函数本身,而在于类型的特征范围。
Rust中的每个泛型类型都有一个隐式的Sized
界限:由于在大多数情况下这是正确的,因此决定不强迫开发人员每次都将其写出来。但是,如果仅在某种类型的引用后面使用此类型,如此处所述,则可能需要通过指定T: ?Sized
来取消此限制。如果添加此代码,则代码可以正常编译:
impl Consumer {
fn consume(eatable: &dyn Eatable) {}
fn consume_generic<T: Eatable + ?Sized>(eatable: &T) {}
}
对于其他问题,主要区别在于静态调度与动态调度。
当您使用通用函数(或语义等效的impl Trait
语法)时,该函数调用是静态分派的。也就是说,对于传递给函数的每种参数类型,编译器都会独立于其他参数生成定义。在大多数情况下,这可能会导致代码更优化,但缺点可能是二进制文件更大,并且API受到某些限制(例如,您无法轻松地以这种方式创建异构集合)。
使用dyn Trait
语法时,您选择加入动态分派。必要的数据将存储在附加到trait对象的表中,并且将在运行时为每个trait方法选择正确的实现。但是,使用者只需要编译一次。通常这是较慢的,这既是由于间接的原因,又是因为不可能进行单独的优化,但更灵活。
至于建议(请注意,这只是一种观点,而不是事实)-我想说,最好尽可能地坚持使用泛型,并且只有在否则无法实现目标的情况下,才将其更改为特征对象。