【问题标题】:When is dynamic polymorphism necessary (compared with static polymorphism)?什么时候需要动态多态(与静态多态相比)?
【发布时间】:2021-06-07 02:50:48
【问题描述】:

我来自函数式语言(例如 Haskell),我非常喜欢 typeclasses 来实现多态性,这也是一种实现即席重载的结构化方法。

但是,最近我开始了解 OOP 对实际问题进行建模的方式,我很好奇为什么我们需要在 OOP 语言中使用 动态多态性(例如作为Java)。根据我的经验,大多数函数调用都可以在编译时解决,因为许多函数式语言不支持 subtyping

所以我的问题是,在什么样的情况下我们必须使用动态多态而不是编译时多态?我的猜测是:

  • 当我们使用包含对象的子类型系统时,我们无法确定其实际类型(例如,我们有一个包含许多不同类型对象的容器。但是,在这种情况下,为什么不尝试代数数据类型联合类型 来模拟容器的元素类型?)。
  • 我们只有对象,不知道它的方法的真实名称,所以我们必须使用 vptr 表来帮助我们。

【问题讨论】:

  • 这能回答你的问题stackoverflow.com/questions/20783266/… 吗?
  • @salazarin 我想我基本了解了静态多态和动态多态的区别,但是我仍然不明白什么时候需要动态多态。
  • 但是如果你有一个框架,因此不知道将使用哪个类,唯一的方法就是使用动态调度,不是吗?您还必须知道“动态”调度是一种语义解释,诀窍是单个间接足以(c ++ vptr)来实现它(意味着没有复杂的“函数搜索”算法)。成本是不变的。
  • 我们举一个非常简单的例子,一个用户界面。我所知道的所有实现都具有树结构,其节点具有特定的编译时基本类型,但实际的子类型决定了它们的外观和行为。我不知道不使用这种多态性的现实 UI 实现会是什么样子。
  • 我怀疑为任意大而深的树递归定义的代数数据类型将比具有动态调度的普通树执行得更好。这在很大程度上取决于编译器将这个定义转换为完全不同的东西的能力。结果与 OOP 代码产生的结果不会有太大不同。

标签: java oop functional-programming polymorphism dynamic-dispatch


【解决方案1】:

在 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 添加到接口,这允许您使用 lambdas 创建该接口的实例。所以你可以用 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);
    }
}

【讨论】:

  • 非常感谢!高阶函数太棒了!但是,对于多态性,我想使用具有各种参数(不同类型)的相同函数名。在 Haskell 中,我看到的唯一方法是通过类型类(通过静态重载)或只是泛型(有限),这与 OOP 的动态调度相比有很大不同。因此在haskell中,我们可以模拟'OOP-like'动态多态性。但是,在任何情况下我必须使用它而不是简单的静态重载吗?
【解决方案2】:

在很多人的帮助下,目前我在反映了很多设计后得到了一些我想要的答案。由于 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>) { ... }

【讨论】:

  • 在 Rust 中,你不能只做fn f(x: MyTrait) { ... },因为特征对象没有大小。您必须改为使用fn f(x: Box&lt;dyn MyTrait&gt;) {...}。您可以使用存在类型在 Haskell 中复制 Rust 的 dyn 特征模型。
  • @MarkSaving 谢谢!这是我的错误,我将对此做一个注释,然后我将使用 ref 或 boxed dyn。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-10-22
  • 2012-08-23
  • 2014-06-30
  • 2010-09-10
  • 2016-12-26
  • 1970-01-01
相关资源
最近更新 更多