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()
方法就成.
比如,定义两个类 Foo
和 Bar
:
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) — 也就是说, “如果它看起来像鸭子,游起来像鸭子,叫起来像鸭子,那你就认为它是鸭子.”
Foo
和 Bar
是完全无关的两个类型. 他们并没有继承自同一个类,但是只要他们都提供了需要的接口,就都能被 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
方法的时机实在类型已经完全确认之后,而不是在模板刚定义的时候.
为了解决这个问题, Foo
和 Bar
需要公用同一个祖先. 假设我们定义这个祖先为 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.
如果一个类涉及到多种类型的事件的话,比如说想实现一个时间记录器,那么就需要实现多个接口了.