暗无天日

=============>DarkSun的个人博客

Duck Typing vs Type Erasure

原文URL: http://nullprogram.com/blog/2014/04/01/

假设有这么一个 C++ 类.

#include <iostream>

template <typename T>
struct Caller {
  const T callee_;
  Caller(const T callee) : callee_(callee) {}
  void go() { callee_.call(); }
};

Caller可以接受任何类型的参数,只要该参数有 call() 方法就成. 比如,定义两个类 FooBar:

struct Foo {
  void call() const { std::cout << "Foo"; }
};

struct Bar {
  void call() const { std::cout << "Bar"; }
};

int main() {
  Caller<Foo> foo{Foo()};
  Caller<Bar> bar{Bar()};
  foo.go();
  bar.go();
  std::cout << std::endl;
  return 0;
}

这段代码可以正常编译,运行后会显示“FooBar”. 这就是所谓的"鸭子类型"(Duck Typing) — 也就是说, “如果它看起来像鸭子,游起来像鸭子,叫起来像鸭子,那你就认为它是鸭子.” FooBar 是完全无关的两个类型. 他们并没有继承自同一个类,但是只要他们都提供了需要的接口,就都能被 Caller 所用. 这是一种很不一样的多态.

"鸭子类型"一般在动态语言中比较常见. 不过通过模板,像 C++ 这样的静态强类型语言也能使用"鸭子类型",而无需损害其保障类型安全的能力. without sacrificing any type safety.

Java Duck Typing

让我们再试试Java的泛型.

class Caller<T> {
    final T callee;
    Caller(T callee) {
        this.callee = callee;
    }
    public void go() {
        callee.call();  // compiler error: cannot find symbol call
    }
}

class Foo {
    public void call() { System.out.print("Foo"); }
}

class Bar {
    public void call() { System.out.print("Bar"); }
}

public class Main {
    public static void main(String args[]) {
        Caller<Foo> f = new Caller<>(new Foo());
        Caller<Bar> b = new Caller<>(new Bar());
        f.go();
        b.go();
        System.out.println();
    }
}

这几乎就是上一个程序的翻版, 但是该程序在编译时会由于类型擦除(type erasure)而报错. 与C++的模板不同,这里 Caller 编译后只会产生一个版本的类, 其中 T 会变成 Object. 由于Object类型并没有 call() 方法,因此编译会失败. 这里泛型的作用只是推迟了编译器对类型的检查而已.

C++模板有点类似于宏, 它由编译器负责进行扩展,每一个类型的参数都会产生不同版本的实现. 检查 call 方法的时机实在类型已经完全确认之后,而不是在模板刚定义的时候.

为了解决这个问题, FooBar 需要公用同一个祖先. 假设我们定义这个祖先为 Callee.

interface Callee {
    void call();
}

Caller 的实现中需要声明T为 Callee 的子类.

class Caller<T extends Callee> {
    // ...
}

现在编译可以通过了,因为 Callee 中有 call() 方法. 最后实现接口 Callee.

class Foo implements Callee {
    // ...
}

class Bar implements Callee {
    // ...
}

这已经不能算是"鸭子类型"了, 只是普通的多态而已. 类型擦除机制使得在Java中无法实现"鸭子类型"(除非使用反射机制).

Signals and Slots and Events! Oh My!

"鸭子类型" 在是现在观察者模型时非常有用,它无需让你定义那么多的模板(boilerplate). 它并不要求一个类必须 继承自特定的类 或者实现特定的接口(interface). 这里有一个很好的例子: the various signal and slots systems for C++. 相比之下Java 需要让所有的类型都实现EventListener接口:

  • KeyListener
  • MouseListener
  • MouseMotionListener
  • FocusListener
  • ActionListener, etc.

如果一个类涉及到多种类型的事件的话,比如说想实现一个时间记录器,那么就需要实现多个接口了.