问题描述
我来自函数式语言(例如 Haskell),我非常喜欢类型类来实现多态性,这也是实现临时重载的结构方法。
然而,最近我开始理解OOP模拟实际问题的方式,我很好奇为什么我们需要OOP语言中的动态多态(例如作为 Java)。根据我的经验,大多数函数调用可以在编译时解决,因为许多函数式语言不支持子类型。
所以我的问题是,在什么样的情况下我们必须使用动态多态而不是编译时多态?我的猜测是:
- 当我们使用包含对象的子类型系统时,我们无法确定其实际类型(例如,我们有一个包含许多不同类型对象的容器。但是,在这种情况下,为什么不尝试代数数据类型 或 联合类型 来建模容器的元素类型?)。
- 我们只有对象,我们不知道它的方法的真实名称,所以我们必须使用vptr表来帮助我们。
解决方法
在 Haskell 中,我们用高阶函数替换了“动态多态”。
考虑以下问题:我们要定义一个表示谓词的类型。我们最终会在实现我们的列表时使用这种类型的 Predicate,以便我们可以定义过滤器功能。我们希望能够轻松定义相等谓词、小于谓词,并且能够通过用“and”连接两个谓词来组合它们。
合理的 Java 尝试如下所示。
interface Predicate<T> {
public abstract boolean predicate(T x);
}
class EqualsPredicate<T> implements Predicate<T> {
private final T value;
public EqualsPredicate(T value) {
this.value = value;
}
@Override
public boolean predicate(T x) {
return this.value.equals(x);
}
}
class LessPredicate<T implements Comparable<T>> implements Predicate<T>{
private final T value;
public LessPredicate(T value) {
this.value = value;
}
@Override
public boolean predicate(T x) {
return this.value.compareTo(x) < 0;
}
}
class AndPredicate<T> implements Predicate<T> {
private final Predicate<T> p1;
private final Predicate<T> p2;
public AndPredicate(Predicate<T> p1,Predicate<T> p2) {
this.p1 = p1;
this.p2 = p2;
}
@Override
public boolean predicate(T x) {
return p1.predicate(x) && p2.predicate(x);
}
}
在 Haskell 中,这个难题的答案是显而易见的。我们只是定义
type Predicate t = t -> Bool
makeEqualsPredicate :: Eq t => t -> Predicate t
makeEqualsPredicate = (==)
makeLessPredicate :: Ord t => t -> Predicate t
makeLessPredicate = (<)
makeAndPredicate :: Predicate t -> Predicate t -> Predicate t
makeAndPredicate p1 p2 x = p1 x && p2 x
-- or,even more succinctly,makeAndPredicate = liftA2 (&&)
Java 允许通过子类化来“动态分派”方法。 Haskell 允许通过高阶函数“动态调度”函数。
等等,你说。 Predicate 是一个只有一个方法的接口。如果我们想要两种方法,我们该怎么做?
好吧,如果一个方法的接口对应一个函数,那么两个方法的接口必须对应一对函数。这就是所谓的“组合优于继承”的 OOP 原则。
所以我们总是可以用 Haskell 风格的高阶函数替换 Java 风格的动态多态性。
事实上,您实际上也在现代 Java 中看到了这一观察结果。从 Java 8 开始,您可以使用一种方法将注释 @FunctionalInterface
添加到接口,这允许您使用 lambda 创建该接口的实例。所以你可以用 Java 8 编写
@FunctionalInterface
interface Predicate<T> {
public abstract boolean predicate(T x);
public static Predicate<J> makeEqualsPredicate(J t) {
return (x) -> t.equals(x);
}
public static Predicate<J implements Comparable<J>> makeLessPredicate(J t) {
return (x) -> t.compareTo(x) < 0;
}
public Predicate<T> makeAndPredicate(Predicate<T> other) {
return (x) -> this.predicate(x) && other.predicate(x);
}
}
,
在很多人的帮助下,目前我在反映了很多设计后得到了一些我想要的答案。由于 Rust 对静态和动态多态都有很好的支持,我将在这个答案中使用 Rust 来证明我的观点。
我现在对动态调度有 2 点:用户友好的可扩展性和较小的编译大小。
第 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对象,这意味着程序将使用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
}
通过这种方式,不需要动态多态,但是,请考虑以下情况:您是已设计库的用户,并且您无法访问更改定义,例如 {{1 }} 图书馆内。现在您想要创建自己的 enums
类型,并且想要重用现有逻辑的代码。如果您正在使用动态调度,那么工作很简单:只需为您的自定义容器类型制作另一个 ContainerType
,一切都很好。不幸的是,如果您使用的是该库的静态版本,那么实现这个目标可能会变得有点困难...
第 2 点:较小的编译大小
像 Rust 这样的语言可能需要多次编译一个泛型函数,对于它所使用的每种类型编译一次。这可能会使二进制文件变大,这种现象在 C++ 圈中称为代码膨胀。 让我们考虑一个更简单的情况:
impl
如果你有很多像function trait MyTrait {
fn method(&self,...) -> ReturnType;
}
fn f(x: MyTrait) { ... } //MyTrait is unsized,this is unvalid rust code
fn g<T: MyTrait>(x: T) { ... }
这样的函数,编译出来的大小可能会越来越大。然而,这应该不是什么大问题,因为如今我们大多数人都可以忽略代码大小以获得充足的内存。
结论
简而言之,尽管静态多态比动态多态有很多优势,但仍有一些角落动态调度可以更好地工作。就个人而言,我真的很喜欢 Haskell-like 处理多态性的方式(这也是我喜欢 Rust 的原因)。我认为这不是最终的最佳和完整答案,欢迎讨论!
组合策略
我突然想到为什么不结合静态和动态策略?为了让用户进一步扩展我们的模型,我们可以留一个小洞供用户以后填写,例如:
g
不过这样一来,像trait Custom {
fn method(&self) -> ReturnType;
}
enum Object {
A(Type0),B(Type1),...
Hole(Box<dyn Custom>)
}
这样的一些操作实现起来可能有点难度,不过我觉得这还是一个很有趣的想法。
更新
Haskell 的存在类型也具有与OOP语言中的动态多态相似的功能和实现:
clone
我还发现这个存在类型可以用来隐藏类型的一些细节,并为用户提供更清晰的界面。
编辑
谢谢@MarkSaving。 Point 2 的代码中有一个错误,dyn trait 对象没有调整大小,因此应该更改为引用或装箱的 dyn:
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