我有这个代码:
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) {}
}
错误:
the size for values of type
dyn Eatable
cannot be known at compilation time
我觉得很奇怪。我有一个方法,它从字面上接受一个 dyn Eatable
并编译得很好,所以该方法以某种方式知道 Eatable
的大小。通用方法 (consume_generic
) 将针对每种使用的类型正确编译以提高性能,而 consume
方法则不会。
于是出现了几个问题:为什么会编译出错? 在方法体内是否有一些东西可以让我做一些我在另一种方法中做不到的事情?我应该在什么时候更喜欢一个而不是另一个?
旁注:我也针对 Swift 语言提出了这个问题:Differences generic protocol type parameter vs direct protocol type .在 Swift 中,我得到了相同的编译错误,但底层错误是不同的:协议(protocol)/特征不符合自身(因为 Swift 协议(protocol)可以包含初始化器、静态事物等,这使得一般引用它们变得更加困难)。我也在 Java 中尝试过,我相信泛型类型已被删除,并且完全没有区别。
最佳答案
问题不在于函数本身,而在于类型的特征边界。
Rust 中的每个泛型类型都有一个隐式的 Sized
绑定(bind):因为这在大多数情况下都是正确的,所以决定不强制开发人员每次都写出来。但是,如果您仅在某种引用后面使用此类型,就像您在此处所做的那样,您可能希望通过指定 T: ?Sized
来解除此限制。如果你添加这个,你的代码将编译正常:
impl Consumer {
fn consume(eatable: &dyn Eatable) {}
fn consume_generic<T: Eatable + ?Sized>(eatable: &T) {}
}
至于其他问题,主要区别在于静态与动态调度。
当您使用通用函数(或语义上等效的 impl Trait
语法)时,函数调用是静态调度的。也就是说,对于您传递给函数的每种类型的参数,编译器都会独立于其他参数生成定义。在大多数情况下,这可能会产生更优化的代码,但缺点可能是更大的二进制文件大小和 API 中的一些限制(例如,您无法通过这种方式轻松创建异构集合)。
当您使用dyn Trait
语法时,您选择了动态调度。必要的数据将存储到附加到 trait 对象的表中,并且将在运行时选择每个 trait 方法的正确实现。然而,消费者只需要编译一次。这通常较慢,这既是由于间接,也是由于不可能进行个别优化,但更灵活。
至于建议(请注意,这是一种观点,而不是事实)——我认为最好尽可能坚持使用泛型,并且只有在无法通过其他方式实现目标时才将其更改为特征对象。
关于rust - 通用特征边界方法与 'direct' 特征方法的区别,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58979396/