`dyn`和泛型有什么区别?
我正在阅读一些代码,它有一个consume函数可以让我传递我自己的函数f。
fn consume<R, F>(self, _timestamp: Instant, len: usize, f: F) -> Result<R>
where
F: FnOnce(&mut [u8]) -> Result<R>,
我写了一些类似的代码,但像这样:
pub fn phy_receive(
&mut self,
f: &mut dyn FnMut(&[u8])
) -> u8 {
公平地说,除了FnOncevs之外,我不知道有什么区别FnMut。使用dyn与泛型类型参数来指定此函数有什么区别?
回答
使用dynwith 类型会导致动态调度(因此是dyn关键字),而使用(受约束的)泛型参数会导致单态。
一般说明
动态调度
动态分派意味着在运行时解析方法调用。就运行时资源而言,它通常比单态更昂贵。
例如,假设您有以下特征
trait MyTrait {
fn f(&self);
fn g(&self);
}
和一个MyStruct实现该特性的结构。如果您使用dyn对该特征的引用(例如&dyn MyTrait),并将对MyStruct对象的引用传递给它,则会发生以下情况:
- 创建了一个“vtable”数据结构。这是包含指向该表
MyStruct的实现方式f和g。 - 指向此 vtable 的指针与
&dyn MyTrait引用一起存储,因此引用将是其通常大小的两倍;&dyn由于这个原因,有时引用被称为“胖引用”。 - 调用
f和g然后将导致使用存储在 vtable 中的指针进行间接函数调用。
单态
单态意味着代码是在编译时生成的。它类似于复制和粘贴。在上一节中使用MyTrait和MyStruct定义,假设您有一个如下所示的函数:
fn sample<T: MyTrait>(t: T) { ... }
然后你传递MyStruct给它:
sample(MyStruct);
发生的情况如下:
- 在编译期间,
sample专门为该MyStruct类型创建函数的副本。用非常简单的术语来说,这就像您复制并粘贴了sample函数定义并替换T为MyStruct:
fn sample__MyStruct(t: MyStruct) { ... }
- 该
sample(MyStruct)呼叫被编译成sample__MyStruct(MyStruct)。
这意味着一般来说,单态在二进制代码大小方面可能更昂贵(因为您本质上是在复制类似的代码块,但用于不同的类型),但没有像动态调度那样的运行时成本。
你的榜样
由于FnMut这只是一个特征,因此上述讨论直接适用于您的问题。这是特征定义:
pub trait FnMut<Args>: FnOnce<Args> {
pub extern "rust-call" fn call_mut(&mut self, args: Args) -> Self::Output;
}
撇开extern "rust-call"怪异不谈,这就是MyTrait上面的特征。这个特性是由某些 Rust 函数实现的,所以这些函数中的任何一个都类似于MyStruct上面的。using&dyn FnMut<...>将导致动态调度,而 using<T: FnMut<...>>将导致单态。
我的 2 美分和一般建议
某些情况需要您使用动态调度。例如,如果你有一个Vec实现某个特征的外部对象,你别无选择,只能使用动态分派。例如,
Vec<Box<dyn Debug>>。
但是,如果这些对象在您的代码内部,您可以使用enum类型和单态。
除此之外,在所有条件相同的情况下,我的建议是,如果可以,请使用单态,如果必须,请使用动态调度。就我个人而言,我注意到在我的大部分代码中,我最终只调用了一次或两次函数。如果你只调用一次,你就可以免费获得单态性,因为二进制代码只会生成一次。另一方面,使用dyn引用可能会导致更简单的代码——你的里程可能会有所不同。