计算机语言中的多态实现

多态(Polymorphism)是指"同一接口,多种实现",是计算机语言中的核心概念。多态可以在编译期实现,也可以在运行时实现。编译期多态通过模板、泛型等机制提供多份代码,而运行时多态通过虚函数、接口、鸭子类型等方式实现动态绑定。本文介绍多态在不同语言中的实现方式。

运行时多态

运行时多态的核心是通过间接机制在运行时确定具体调用哪个实现。本章介绍运行时多态的实现原理,包括基于继承的动态派发、基于类型标签的联合体、基于接口的协议约束、以及基于属性查找的鸭子类型。

基于继承的动态派发

这是面向对象语言中最经典的运行时多态实现方式。通过继承建立类型层次,使用虚函数实现动态派发。

C++ 示例:

C++ 复制代码
class Animal {
public:
    virtual void speak() = 0;  // 纯虚函数
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!\n"; }
};

class Cat : public Animal {
public:
    void speak() override { std::cout << "Meow!\n"; }
};

int main() {
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();
    animal1->speak();  // 输出: Woof!
    animal2->speak();  // 输出: Meow!
    delete animal1;
    delete animal2;
}

实现原理:

编译器为每个包含虚函数的类生成虚函数表(vtable),对象中存储指向 vtable 的指针。调用虚函数时,通过对象的 vtable 指针查找实际函数地址,实现动态绑定。

TS 示例:

TS 复制代码
abstract class Animal {
    abstract speak(): void;
}

class Dog extends Animal {
    speak(): void { console.log("Woof!"); }
}

class Cat extends Animal {
    speak(): void { console.log("Meow!"); }
}

const animal1: Animal = new Dog();
const animal2: Animal = new Cat();
animal1.speak();  // 输出: Woof!
animal2.speak();  // 输出: Meow!

实现原理:

TypeScript 编译为 JavaScript 后,继承关系通过原型链实现。调用方法时,引擎沿原型链向上查找,找到对应的方法实现并调用。

基于类型标签的联合体

联合体通过类型标签记录当前存储的类型,在运行时根据标签选择对应的处理逻辑。

C++ 示例:

C++ 复制代码
#include <variant>
#include <iostream>
#include <string>

struct Circle { double radius; };
struct Rectangle { double width, height; };

using Shape = std::variant<Circle, Rectangle>;

double area(const Shape& shape) {
    return std::visit([](auto&& s) -> double {
        using T = std::decay_t<decltype(s)>;
        if constexpr (std::is_same_v<T, Circle>) {
            return 3.14 * s.radius * s.radius;
        } else if constexpr (std::is_same_v<T, Rectangle>) {
            return s.width * s.height;
        }
    }, shape);
}

int main() {
    Shape s1 = Circle{5.0};
    Shape s2 = Rectangle{3.0, 4.0};
    std::cout << area(s1) << "\n";  // 输出: 78.5
    std::cout << area(s2) << "\n";  // 输出: 12
}

实现原理:

std::variant 内部存储类型标签和足够大的存储空间。std::visit 根据类型标签在运行时分发到对应的处理分支。

TS 示例:

TS 复制代码
type Circle = { kind: 'circle'; radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Shape = Circle | Rectangle;

function area(shape: Shape): number {
    switch (shape.kind) {  // 根据类型标签分发
        case 'circle':
            return 3.14 * shape.radius * shape.radius;
        case 'rectangle':
            return shape.width * shape.height;
    }
}

const s1: Shape = { kind: 'circle', radius: 5 };
const s2: Shape = { kind: 'rectangle', width: 3, height: 4 };
console.log(area(s1));  // 输出: 78.5
console.log(area(s2));  // 输出: 12

实现原理:

TypeScript 的联合类型(Union Types)通过判别字段(如 kind)在运行时区分具体类型。编译后的 JavaScript 使用 switch 语句根据标签分发到不同分支。

基于接口的协议约束

接口定义了一组方法签名,任何实现这些方法的类型都满足该接口。这种方式强调"能做什么"(协议),而非"是什么"(类型层次)。

C++ 示例:

C++ 没有提供接口的语言特性,通过继承抽象基类实现接口约束。

cpp 复制代码
class Drawable {
public:
    virtual void draw() = 0;
};

class Circle : public Drawable {
public:
    void draw() override { std::cout << "Circle\n"; }
};

class Square : public Drawable {
public:
    void draw() override { std::cout << "Square\n"; }
};

void render(Drawable* shape) {
    shape->draw();
}

int main() {
    Circle c;
    Square s;
    render(&c);  // 输出: Circle
    render(&s);  // 输出: Square
}

实现原理:

C++ 通过继承和虚函数表实现接口约束,机制上与继承派发相同。区别在于语义:接口强调协议,继承强调类型关系。

TS 示例:

TypeScript 提供 interface 关键字定义接口,类通过 implements 实现接口。

typescript 复制代码
interface Drawable {
  draw(): void;
}

class Circle implements Drawable {
  draw(): void {
    console.log("Circle");
  }
}

class Square implements Drawable {
  draw(): void {
    console.log("Square");
  }
}

function render(shape: Drawable): void {
  shape.draw();
}

const c = new Circle();
const s = new Square();
render(c); // 输出: Circle
render(s); // 输出: Square

实现原理:

TypeScript 的接口在编译期进行类型检查,确保类实现了接口的所有方法。编译为 JavaScript 后,接口信息被擦除,运行时通过对象的方法直接调用。

基于属性查找的鸭子类型(动态语言)

鸭子类型(Duck Typing)的核心思想是"如果它走起来像鸭子,叫起来像鸭子,那它就是鸭子"。不关心对象的类型或接口声明,只关心对象在运行时是否具有所需的属性或方法。

C++ 是静态类型语言,不支持运行时鸭子类型。

TS 示例:

TypeScript 的结构化类型系统支持鸭子类型,只要对象具有所需的方法或属性,就满足类型要求。

typescript 复制代码
function render(shape: { draw(): void }) {
  shape.draw();
}

class Circle {
  draw() {
    console.log("Circle");
  }
}

class Square {
  draw() {
    console.log("Square");
  }
}

const c = new Circle();
const s = new Square();
render(c); // 输出: Circle
render(s); // 输出: Square

// 可以传递普通对象
render({ draw: () => console.log("Triangle") }); // 输出: Triangle

实现原理:

TypeScript 在编译期检查对象是否具有所需的属性或方法,不要求显式声明类型关系。编译为 JavaScript 后,运行时通过对象的属性直接调用方法,真正实现运行时鸭子类型。

编译期多态(C++)

C++ 通过模板(Templates)实现编译期多态。编译器根据模板参数生成多份代码,每份代码针对特定类型进行优化。本章介绍 C++ 模板的三种形式:函数模板、类模板、以及模板特化。

函数模板

函数模板允许编写适用于多种类型的通用函数,编译器根据调用时的类型参数生成具体函数。

cpp 复制代码
template <typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    std::cout << max(3, 7) << "\n";        // 生成 max<int>
    std::cout << max(3.5, 2.1) << "\n";    // 生成 max<double>
}

实现原理:

编译器在编译期根据调用的类型参数实例化模板,生成对应类型的函数代码。每种类型都有独立的函数实现,没有运行时开销。

类模板

类模板允许编写适用于多种类型的通用类,常用于容器、智能指针等场景。

cpp 复制代码
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(T value) { data.push_back(value); }
    T pop() { T v = data.back(); data.pop_back(); return v; }
};

int main() {
    Stack<int> s1;      // 生成 Stack<int>
    s1.push(10);

    Stack<string> s2;   // 生成 Stack<string>
    s2.push("hello");
}

实现原理:

编译器根据模板参数生成不同的类定义。每个类型参数组合对应一个独立的类,在编译期完成所有类型检查。

模板特化

模板特化允许为特定类型提供定制化实现,在通用逻辑的基础上处理特殊情况。

cpp 复制代码
template <typename T>
class Printer {
public:
    void print(T value) { std::cout << value << "\n"; }
};

// 为 bool 类型提供特化实现
template <>
class Printer<bool> {
public:
    void print(bool value) {
        std::cout << (value ? "true" : "false") << "\n";
    }
};

int main() {
    Printer<int> p1;
    p1.print(42);       // 输出: 42

    Printer<bool> p2;
    p2.print(true);     // 输出: true
}

实现原理:

编译器优先选择特化版本。遇到特化类型时使用特化实现,其他类型使用通用模板。这在编译期完成,没有运行时判断。

编译期多态(TypeScript)

TypeScript 通过泛型(Generics)实现编译期多态。类型系统在编译期进行类型检查和推断,编译为 JavaScript 后类型信息被擦除。本章介绍 TypeScript 泛型的三种形式:泛型函数、泛型类、以及条件类型。

泛型函数

泛型函数允许编写适用于多种类型的通用函数,TypeScript 在编译期推断或检查类型参数。

typescript 复制代码
function max<T>(a: T, b: T): T {
  return a > b ? a : b;
}

console.log(max(3, 7)); // 推断为 max<number>
console.log(max(3.5, 2.1)); // 推断为 max<number>
console.log(max("a", "b")); // 推断为 max<string>

实现原理:

TypeScript 在编译期根据调用的参数类型推断类型参数,进行类型检查。编译为 JavaScript 后,类型信息被擦除,生成的代码与普通函数相同,没有泛型痕迹。

泛型类

泛型类允许编写适用于多种类型的通用类,在实例化时指定具体类型。

typescript 复制代码
class Stack<T> {
  private data: T[] = [];

  push(value: T): void {
    this.data.push(value);
  }
  pop(): T {
    return this.data.pop()!;
  }
}

const s1 = new Stack<number>(); // Stack<number>
s1.push(10);

const s2 = new Stack<string>(); // Stack<string>
s2.push("hello");

实现原理:

TypeScript 在编译期检查类型参数的使用是否正确。编译为 JavaScript 后,生成的是普通类,类型参数信息被擦除。运行时只有一份类代码,不同类型共享同一实现。

相关推荐
小龙报3 小时前
《VScode搭建教程(附安装包)--- 开启你的编程之旅》
c语言·c++·ide·vscode·单片机·物联网·编辑器
kyle~3 小时前
C++20--- concept 关键字 为模板参数提供了编译期可验证的约束机制
运维·c++
CS_浮鱼3 小时前
【C++进阶】异常
开发语言·c++
QT 小鲜肉3 小时前
【C++基础与提高】第十一章:面向对象编程进阶——继承与多态
java·linux·开发语言·c++·笔记·qt
艾莉丝努力练剑3 小时前
【C++:封装红黑树】C++红黑树封装实战:从零实现MyMap与MySet
c++·stl·set·map·红黑树·平衡二叉树
序属秋秋秋4 小时前
《Linux系统编程之进程基础》【进程入门】
linux·运维·c语言·c++·进程·系统编程·fork
点云SLAM4 小时前
Boost库中Boost.PropertyTree使用和实战示例
开发语言·c++·josn·boost库·参数读取
晨非辰4 小时前
【数据结构】排序详解:从快速排序分区逻辑,到携手冒泡排序的算法效率深度评测
运维·数据结构·c++·人工智能·后端·深度学习·排序算法
草莓熊Lotso5 小时前
C++ 二叉搜索树(BST)完全指南:从概念原理、核心操作到底层实现
java·运维·开发语言·c++·人工智能·经验分享·c++进阶