运行期的多态,是很多高级语言的很重要的特性。我们可以在运行期根据实际创建的对象,决定实际所要运行的函数。
C++可以通过virtual member function实现运行期的多态,同样,在Go我们可以通过interface实现类似的机制。虽然C++的virtual和Go的interface本质上还是有很多区别,但是不妨碍我们深入探讨一下关于如何实现运行期多态。
Note:
- 不去理解实现机制,并不影响我们使用C++或者Go(只要遵循语言的标准并不会有什么麻烦)。当然,如果能更深入的理解实现,本身有利于加深我们对代码的理解。
- 我们讨论的的实现很可能只代表主流编译器的实现,并不一定代表语言的标准。编译器需要遵循语言规范去实现,但是如何实现往往是编译器自己的决定。
C++多态
我们都知道C++的多态,通过基类里面把成员函数定义为virtual,继承类可以重新实现这个函数。使用一个基类的指针指向对象,并且通过该指针执行那个函数,具体执行的是基类还是继承类的函数,取决于运行期该对象的类型。C++的多态/继承当然没有那么简单,还有非常多的细节特性,想要完全说明白几乎不可能(建议去翻C++标准或者经典类的书)。我们这里只需要用最简单的例子,去说明实现的原理。
例如下面的代码:
1 | class Base { |
Hello D
1 |
|
class D : vtable –> ———–
| D::func |
———–
| null |(null for array end)
———–
1 |
|
class D : vtable –> ————–
| Base::func |
————–
| null |(null for array end)
————–
1 |
|
vtable
p = new D() —–> ———— ———–
| vptr | ——–>| D::func |
———— ———–
| | | null |
| D object | ———–
| |
————
1 |
|
type Base interface {
func()
}
type D struct {}
func (d *D) func() {
fmt.Println(“Hello D”)
}
/// main.go
var p Base = new(D)
p.func()
1 |
|
Hello D
1 |
|
Base –> ————-
| value |
————- ———
| itable | —–> | func |
————- ———
| null |
———
1 |
|
Base –> ————- ——————-
| value |————————————-> | address of D() |
————- ——— ——————-
| itable | —–> | func |—————
————- ——— | ————-
| null | |———-> | D::func() |
——— ————-
所以,当我们执行`p->func()`时,编译器已经知道具体执行的是`D::func()`
## 总结
C++和Go interface内部实现机制并不一样,但是都可以实现运行期的多态绑定。就是说,我们可以根据运行期实际创建的对象是什么,来决定运行的函数。
C++通过编译器静态产生vtable的方式实现,但是需要在运行期进行vtable查找,会带来运行期开销。
Go interface在运行期计算itable,好处是在执行的时候基本上是O(1)开销。