4

Duck Typing vs Type Erasure

 3 years ago
source link: https://www.lujun9972.win/blog/2017/03/07/duck-typing-vs-type-erasure/index.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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.

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK