highlight: a11y-dark theme: juejin
多态是面向对象编程中的一个核心概念,它允许我们以统一的方式处理不同类型的对象,同时还能保持各个类型特有的行为。
换句话说:对于一个函数/方法,它会根据传入的对象类型不同,做出不同的动作就是多态。多态本质上是为了实现代码复用。不管是用哪种方法实现,把函数中通用的部分抽象出来,从而减轻代码书写负担也便于后续的维护。由于本人之前比较熟悉的语言是c++,故在此分别讨论两种语言实现多态的方式,对比学习。
c++
c++实现多态主要有三种方式:函数重载、模板、面向对象。
函数重载
重载是最为简单的,根据我们传入的参数不同,由编译器帮我们选择对应的函数去执行。拿两个数相加举例子:
int add(int a, int b) {
return a + b;
}
folat add(float a, float b) {
return a + b;
}
可以看出这种实现方式简单好用;缺点就是代码量较大不易维护,对于每个参数不同的版本我们都需要重新进行重载,书写对应的函数体。那么有没有更简单快捷的方式呢?模板。
模板
模板实现多态的思路其实和函数重载类似,只不过它把书写众多函数的任务交给了编译器,由编译器根据我们传入的参数去生成特定版本的函数。
template <typename T>
auto add(T a, T b) -> decltype(a + b) {
return a + b;
};
这种实现多态的方式称之为编译器多态,为了与下文运行期多态区分。
面向对象
通过使用面向对象中的继承体系来实现运行期多态也是一种思路。多态的发生有三个条件:
- 向上转型
- 指针或引用
- 虚函数调用
class Animal {
public:
virtual void speak() {};
};
class Dog: public Animal {
public:
Dog(string name):name(name) {};
virtual void speak() override {
std::cout << "dog " << name << " speak wang wang" << std::endl;
}
string name;
};
class Cat: public Animal {
public:
Cat(string name):name(name) {};
virtual void speak() override {
std::cout << "cat " << name << " speak mi mi" << std::endl;
}
string name;
};
void life(Animal* a) {
a->speak();
}
int main() {
Animal* a = new Dog("wangcai");
life(a);
a = new Cat("tangtang");
life(a);
return 0;
}
life函数的形参是基类Animal类型的指针,通过传入静态类型为Animal而动态类型为派生类类型的指针实参,就可以根据动态类型来调用虚函数了。
运行时类型识别
c++虽然不直接支持反射,但它支持类型识别。我们可以通过RTTI(运行时类型识别)来实现多态。之前我一直搞不懂运行时类型识别和反射的关系。现在先简单做个总结。
- RTTI允许在运行时识别和使用类型信息;而反射则允许在运行时修改系统行为。
- RTTI专注于发现和使用对象的类型信息,而反射用于创建和操纵对象。具体来说,RTTI会在运行期间自动计算类型信息,而反射则需要显式请求,且可能造成更大的系统开销。
反射的使用场景:
- 序列化(Serialization), in custom binary format or in XML, JSON, XDR, etc.
- 反序列化(Deseriallization),从序列中重建了对象实例与关系。
- 远程方法调用, remote procedure calls (RPC) / remote method invocation (RMI)。
- 对象/关系数据映射(O/R mapping)eg. Hibernate,作为虚拟对象数据库,实现数据和对象的持久化。
- 数据绑定(Data Binding),实现数据对象与关系的可视化,与交互控制调整。
- 某些软件设计模式的自动化和半自动化实现。
具体到c++,我们可以使用std::any
来实现。std::any
可以接受任意类型,并且把存储的类型记录下来。但是一个致命的缺点是在每次使用的时候,使用方必须知道目前存储的类型,进行强制类型转换后才可以使用。一个简单的样例如下
#include <iostream>
#include <any>
#include <typeinfo>
//std::any需要c++17
int main() {
std::any var = 3.33;
if (var.type() == typeid(int)) {
std::cout << "int" << std::endl;
std::cout << std::any_cast<int>(var) << std::endl;
}
else if (var.type() == typeid(double)) {
std::cout << "double" << std::endl;
std::cout << std::any_cast<double>(var) << std::endl;
}
return 0;
}
Go
go语言实现多态也有多种方法。go的接口类似于c++的虚函数,只不过它的实现是隐式的,实现了相应接口的类就可以作为实参传递给相应的接口类型;利用空接口和运行时类型识别来实现多态;泛型实现多态。由于我在另一篇文章中已经讲述了go语言的反射和泛型。故在此只做接口的实现。与上面的c++继承实现多态类似。
package main
import (
"fmt"
)
type Animal interface {
speak()
}
type Dog struct {
name string
}
func (d *Dog) speak() {
fmt.Printf("Dog %s speak wang wang\n", d.name)
}
type Cat struct {
name string
}
func (c *Cat) speak() {
fmt.Printf("Cat %s speck mi mi\n", c.name)
}
func life(a Animal) {
a.speak()
}
func main() {
//new只分配内存,不可以初始化或赋值
var d = new(Dog)
d.name = "wangcai"
var c = &Cat{name: "mimi"}
life(d)
life(c)
}