掘金 后端 ( ) • 2024-05-05 16:58

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)
}