在很多人的帮助下,目前我在反映了很多设计后得到了一些我想要的答案。由于 Rust 对静态和动态多态性都有很好的支持,我将在这个答案中使用 Rust 来证明我的观点。
我现在对动态调度有两点:用户友好的可扩展性和更小的编译大小。
第 1 点:用户友好的可扩展性
很多人认为动态调度适合你有一个容器来收集各种对象(当然,不同类型)的情况。例如:
trait MyTrait {
fn method(&self, ...) -> ReturnType;
}
type MyContainer = Vec<Box<MyTrait>>;
fn main() {
...
//the_container : MyContainer
the_container.iter().map(... { x.method(...) } ...) //only demo code
}
在上面的代码中,在编译时,我们只知道元素是trait objects,这意味着程序将使用vptr-like策略来查找哪个方法在main函数中执行表达式时使用。
但是,还有另一种方法可以实现几乎相同的功能:
enum MyContainerTypes {
Type0 A,
Type1 B,
...
}
impl MyTrait for MyContainerType {
fn method(&self, ...) -> ReturnType {
match self {
Type0 a => {...},
Type1 b => {...},
...
}
}
}
type MyContainer = Vec<MyContainerType>;
fn main() {
...
//my_container : MyContainer
my_container.iter().map(... { x.method(...) } ...); //demo
}
通过这种方式,不需要动态多态性,但是,请考虑以下情况:您是已设计库的用户,并且您无权访问更改定义,例如enums图书馆内。现在你想创建自己的ContainerType 类型并且你想重用现有逻辑的代码。如果您使用动态调度,工作很简单:只需创建另一个您的自定义容器类型的impl,一切都很好。不幸的是,如果您使用的是静态版本的库,那么实现这个目标可能会有点困难......
第 2 点:较小的编译大小
像 Rust 这样的语言可能需要多次编译一个泛型函数,每种类型都编译一次。这可能会使二进制文件变大,这种现象在 C++ 圈子中称为代码膨胀。
让我们考虑一个更简单的情况:
trait MyTrait {
fn method(&self, ...) -> ReturnType;
}
fn f(x: MyTrait) { ... } //MyTrait is unsized, this is unvalid rust code
fn g<T: MyTrait>(x: T) { ... }
如果你有很多函数如g,编译后的大小可能会越来越大。然而,这应该不是一个大问题,因为我们大多数人现在都可以忽略代码大小以获得充足的内存。
结论
简而言之,虽然静态多态比动态多态有很多优势,但动态调度还有一些地方可以做得更好。就我个人而言,我真的很喜欢 Haskell-like 处理多态性的方式(这也是我喜欢 Rust 的原因)。我认为这不是最终的最佳和完整答案,欢迎讨论!
组合策略
突然想到为什么不将静态和动态策略结合起来呢?为了让用户进一步扩展我们的模型,我们可以留一个小洞供用户稍后填写,例如:
trait Custom {
fn method(&self) -> ReturnType;
}
enum Object {
A(Type0),
B(Type1),
...
Hole(Box<dyn Custom>)
}
不过这样一来,像clone这样的一些操作可能实现起来有点困难,但我觉得这还是一个有趣的想法。
更新
Haskell 的 存在类型 也具有与 OOP 语言中的动态多态性类似的功能和实现:
data Obj = forall a. (Show a) => Obj a
xs :: [Obj]
xs = [Obj 1, Obj "foo", Obj 'c']
doShow :: [Obj] -> String
doShow [] = ""
doShow ((Obj x):xs) = show x ++ doShow xs
我还发现这个存在类型可以用来隐藏类型的一些细节,提供更干净的界面供用户使用。
编辑
感谢@MarkSaving。 第 2 点的代码有一个错误,dyn trait 对象没有调整大小,因此应该改为引用或装箱的 dyn:
fn f(x: Box<dyn MyTrait>) { ... }