多态(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 后,生成的是普通类,类型参数信息被擦除。运行时只有一份类代码,不同类型共享同一实现。