使用 dyn
和类型一起使用会导致动态分派(因此有 dyn
关键字),而使用(受限的)泛型参数会导致单态性。
一般解释
动态分派
动态分派意味着方法调用在运行时解析。通常从运行时资源方面来说比单态性更昂贵。
例如,假设您有以下特征
trait MyTrait {
fn f(&self);
fn g(&self);
}
有一个结构体MyStruct
实现了某个特质。如果你使用 &dyn MyTrait
,并将MyStruct
对象的引用传递给它,那么会发生以下情况:
- 创建一个"vtable"数据结构。这是一个包含指针
f
和g
的MyStruct
实现的表。
- 将指向该vtable的指针存储在
&dyn MyTrait
引用中,因此该引用的大小将是其通常大小的两倍;有时候&dyn
引用因此被称为"fat references"。
- 然后调用
f
和g
将导致使用存储在vtable中的指针进行间接函数调用。
单态化
单态化意味着代码在编译时生成。类似于复制和粘贴。使用上一节中定义的MyTrait
和MyStruct
,想象一下你有一个如下所示的函数:
fn sample<T: MyTrait>(t: T) { ... }
当你将一个MyStruct
传递给它时:
sample(MyStruct)
发生的情况如下:
- 在编译时,会为
MyStruct
类型创建一个sample
函数的副本。简单来说,就好像你复制并粘贴了sample
函数的定义,并将T
替换为MyStruct
:
fn sample__MyStruct(t: MyStruct) { ... }
sample(MyStruct)
调用被编译成sample__MyStruct(MyStruct)
。
这意味着通常情况下,单态化在二进制代码大小方面可能更加昂贵(因为您实际上是为不同的类型复制类似的代码块),但与动态分派不同,它没有运行时成本。
单态化在编译时间方面也通常更加昂贵:因为它实际上是代码的复制粘贴,使用单态化的代码库往往需要更长的编译时间。
你的例子
由于FnMut
只是一个trait,因此上述讨论直接适用于您的问题。以下是该trait的定义:
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
。使用&dyn FnMut<...>
将导致动态调度,而使用<T: FnMut<...>>
将导致单态性。
我的建议和通用建议
某些情况下需要使用动态分派。例如,如果您有一个实现某个特征的外部对象的Vec
,则除了使用动态分派之外别无选择。例如,Vec<Box<dyn Debug>>
。但是,如果这些对象在您的代码内部,则可以使用enum
类型和单态性。
如果您的特征包含关联类型或通用方法,则必须使用单态性,因为这些特征不是对象安全的。
其他所有内容相等时,我的建议是在您的代码库中选择一个偏好并坚持使用它。从我所见,大多数人倾向于默认使用通用和单态性。