Dynamic Dispatch.

考虑类 Point

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point
attr_accessor :x, :y # define getter/setter for fields x, y
def initialize(x, y)
@x = x
@y = y
end
def disFromOrigin
Math.sqrt(@x * @x + @y * @y)
end
def disFromOrigin2
Math.sqrt(x * x + y * y) # call getters of x, y instead of directly accessing them
end
end

接下来我们来考虑类 Point 的一个有趣的子类 PolarPoint:它内部采用弧度表示法而非坐标表示法来表示某个点,其余方法与父类等价。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PolarPoint < Point
def initialize(r, theta)
@r = r
@theta = theta
end
def x
@r * Math.cos(@theta)
end
def y
@r * Math.sin(@theta)
end
def x= a
b = y # avoid mutiple calls to method y
@theta = Math.atan(b / a)
@r = Math.sqrt(a*a + b*b)
self
end
def y= b
a = y
@theta = Math.atan(b / a)
@r = Math.sqrt(a*a + b*b)
self
end
def distFromOrigin
@r
end
# No need to override distFromOrigin2, it already works!
end

注意,在 initialize 中没有使用 super 调用父类中的同名初始化方法,所以实际上类 PolarPoint 是没有定义实例变量 @x@y 的;但它 override 了方法 x, x=, yy=,使得用户无法将其与类 Point 进行区别。而在 Java/C++ 中,子类默认将继承父类中的实例变量,你可以选择是否使用它们。

The key point of this example is that the class does not override distFromOrigin2, but the inherited method works correctly.

这是为什么呢?我们来看 PolarPoint 类从父类中继承的 DistFromOrigin2 方法的定义:

1
2
3
def distFromOrigin2
Math.sqrt(x * x + y * y)
end

distFromOrigin 方法不同,distFromOrigin2 方法使用调用其他方法 (self.x()self.y()) 的结果来计算距离而不是直接使用实例变量 @x@y 中储存的值。

然而,PolarPoint 类 override 了方法 xy;这使得继承自 Point 类的方法 DistFromOrigin2 的表现 (behavior) 发生了改变。虽然是相同的代码,但在子类与父类中却分别调用了不同的方法。

这一 semantic 被称之为 dynamic dispatch。又名 late bindingvirtual method call。这是一个十分 OOP 的 semantic —— 它涉及到环境中 self 的特殊处理。

我们看到一个熟悉的名字 virtual method:事实上,C++ 中的虚函数采用的也是该 semantics。虚函数的定义是:通过基类指针调用虚函数时,若指针指向基类对象,则被调用的是基类的虚函数;若指向的是派生类对象,则被调用的是派生类的虚函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <string>

using namespace std;

class Base {
public:
virtual int x() { return 1; }
virtual int y() { return 1; }
void put_ans() {
cout << x() + y() << endl;
}
};

class Derived : public Base {
public:
int x() { return 2; }
int y() { return 2; }
}

int main() {
Base a;
a.put_ans();
Derived b;
b.put_ans();

return 0;
}

以上这个代码的运行结果为 2\n4;也就是说,派生类 Derived 类由基类中继承的函数 put_ans() 调用的是派生类中被 override 的函数 x()y(),实现了调用同样的方法 put_ans() 在基类与派生类中的不同表现。

注意,在 C++ 中 dynamic dispatch 所依赖的函数需要在基类中添加关键字 virtual 声明其为虚函数;在上例中,若 x()y() 函数未声明为虚函数,代码的运行结果将变为 2\n2