引言
多态(Polymorphism)是面向对象编程和类型系统中的核心概念,它是指在使用相同的接口时,不同类型的对象,会采用不同的实现。
根据类型系统的不同,多态的实现方式也有所差异:
- 在动态类型系统中,多态主要通过鸭子类型(Duck Typing)实现。
- 在静态类型系统中,多态则可以通过参数多态(Parametric Polymorphism)、特设多态(Ad-hoc Polymorphism)和子类型多态(Subtype Polymorphism)来实现。
对于 Python、Ruby 和 JavaScript 等动态类型语言来说,变量不绑定到具体的类型,而是绑定到具体的对象。如果一个对象具有某种方法或属性,并且这些方法或属性可以被正确调用,那么它就可以被认为是实现了某种“接口”或“能力”。鸭子类型的核心思想可以用这句经典的话概括:“If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”,即:如果某个对象“看起来像鸭子,游泳像鸭子,叫声也像鸭子”,那我们就可以认为它是一只鸭子,而不需要关心它的具体类型。
对于 Rust、C++、Java 和 Go 等静态类型语言来说,变量不绑定到具体的对象,而是绑定到具体的类型。这意味着在编译期,编译器会对变量的类型进行严格检查,确保类型匹配和方法调用的正确性。在这些语言中,多态性是通过显式的类型约束或继承机制来实现的,程序员需要明确地定义对象的行为和能力,而不能依赖动态检查。这类语言强调类型的静态绑定,其参数多态、特设多态和子类型多态的支持方式如下:
语言 | 参数多态 | 特设多态 | 子类型多态 |
---|---|---|---|
Rust | 泛型(<T> ) |
trait |
动态分派(trait object ) |
C++ | 泛型(模板) | 函数重载、运算符重载 | 虚函数(virtual) |
Java | 泛型 | 方法重载 | 继承和接口 |
Go | 泛型(1.18 及以后) | 无直接支持(可模拟) | 接口 |
说明如下:
- 参数多态:指代码能够处理不同类型的参数,而不需要事先指定具体的类型。通常通过 泛型 来实现,使得函数、类或接口能够接受不同类型的输入而不牺牲类型安全。例如,通过泛型,我们可以编写通用代码来处理任意类型的数据,而无需为每个类型编写不同的实现。泛型使得我们能够创建能够适用于多种数据类型的函数和类,减少了重复代码。
-
特设多态:指同一种行为有多个不同的实现。比如,加法操作可以有不同的实现:对于整数数据类型,它是数学加法;对于字符串,它是拼接;对于自定义类型,它可能是矩阵加法等。通常,特设多态通过函数重载或运算符重载实现,这些技术允许使用相同的名称或符号(如
+
)进行不同类型的操作。在不同的语言中,特设多态可以通过不同的机制实现,如 C++ 中的函数重载和运算符重载,Java 中的方法重载,或 Rust 中的 trait 实现。 - 子类型多态:指在运行时子类的对象可以被当作父类的引用来使用。这使得父类的引用能够指向任何派生自该父类的对象,从而能够实现灵活的行为扩展和替换。子类型多态通常通过继承、接口和动态分派实现,在调用方法时,系统根据实际对象的类型(而非引用类型)决定调用哪个方法。这类多态是面向对象编程中非常重要的特性,通常通过接口、抽象类和虚方法等机制来实现。
Rust 参数多态
在 Rust 中,参数多态 是通过 泛型 来实现的。泛型允许函数、结构体、枚举和 trait 处理不同类型的数据,而不需要为每种类型单独编写代码。它使得代码更加通用和灵活,可以在编译时根据传入的类型进行具体化,而无需运行时的类型检查。
函数中的泛型
函数中的泛型可以让我们编写通用的函数,不同的类型可以作为参数传入,而不需要为每种类型单独编写不同版本的函数。
例子:
fn multiply<T>(a: T, b: T) -> T {
a * b
}
fn main() {
let int_result = multiply(2, 3); // 整数类型
let float_result = multiply(2.5, 3.0); // 浮点数类型
println!("Integer result: {}", int_result);
println!("Float result: {}", float_result);
}
-
multiply
是泛型函数,输入两个类型为 T 的参数 a 和 b,并返回类型为 T 的值。 -
multiply
在编译时根据实际传入的类型进行替换,可以接受任何类型的参数,只要该类型支持*
操作。
对于泛型函数,Rust 会进行单态化(Monomorphization)处理,也就是在编译时,把所有用到的泛型函数的泛型参数展开,生成若干个函数。
单态化的优势:泛型函数的调用是静态分派(static dispatch),在编译时就一一对应,既保留了多态的灵活性,又没有任何效率的损失,和普通函数调用一样高效。
然而,单态化也有很明显的劣势:
- 编译速度很慢,一个泛型函数,编译器需要找到所有用到的不同类型,一个个编译,所以 Rust 编译代码的速度总被人吐槽,这和单态化脱不开干系。
- 编出来的二进制会比较大,因为泛型函数的二进制代码实际存在 N 份。
- 代码以二进制分发会损失泛型的信息,因为单态化之后,原本的泛型信息被丢弃了。
结构体中的泛型
结构体中的泛型允许你定义可以处理多种类型的结构体,从而提高代码复用性。
例子:
struct Pair<T, U> {
first: T,
second: U,
}
impl<T, U> Pair<T, U> {
fn new(first: T, second: U) -> Self {
Pair { first, second }
}
fn first(&self) -> &T {
&self.first
}
fn second(&self) -> &U {
&self.second
}
}
fn main() {
// 创建第一个 Pair 实例,类型为 (i32, &str)
let pair1 = Pair::new(1, "hello");
println!("Pair 1 - First: {}, Second: {}", pair1.first(), pair1.second());
// 创建第二个 Pair 实例,类型为 (f64, bool)
let pair2 = Pair::new(3.14, true);
println!("Pair 2 - First: {}, Second: {}", pair2.first(), pair2.second());
}
-
Pair
是一个带有泛型的结构体,它可以存储任何类型的两个值,分别由T
和U
来表示。 - 通过泛型,我们可以创建多个类型组合的结构体实例。
我们知道,结构体内部如果有借用的字段,需要显式地标注生命周期。其实在 Rust 里,生命周期标注也是泛型的一部分,一个生命周期 'a 代表任意的生命周期,和 T 代表任意类型是一样的。
枚举中的泛型
在枚举中使用泛型可以使枚举更加灵活和通用。
例子:
enum Option<T> {
Some(T),
None,
}
fn main() {
// 第一个例子:使用 i32 类型
let some_number = Option::Some(10);
let none_value: Option<i32> = Option::None;
if let Option::Some(value) = some_number {
println!("Some number: {}", value);
} else {
println!("No number found");
}
match none_value {
Option::Some(value) => println!("Some value: {}", value),
Option::None => println!("None value"),
}
// 第二个例子:使用 &str 类型
let some_text = Option::Some("Hello, world!");
let none_text: Option<&str> = Option::None;
if let Option::Some(value) = some_text {
println!("Some text: {}", value);
} else {
println!("No text found");
}
match none_text {
Option::Some(value) => println!("Some text: {}", value),
Option::None => println!("None text"),
}
}
-
Option
是一个带有泛型的枚举,它表示一个可能包含某种类型值(Some(T)
)或没有值(None
)的选项。 - 通过泛型,
Option
枚举可以容纳任何类型的数据。
trait 中的泛型
trait 中的泛型使得我们可以定义通用的行为,并为多种类型提供实现。
例子:
trait Summarizable {
fn summarize<T>(&self, item: T) -> String;
}
struct Article {
title: String,
content: String,
}
impl Summarizable for Article {
fn summarize<T>(&self, item: T) -> String {
format!("{}: \"This is a {:?} summary.\"", self.title, item)
}
}
fn main() {
let article = Article {
title: String::from("Rust Programming"),
content: String::from("Rust is a systems programming language."),
};
// 使用不同类型的 item 来调用 summarize 方法,展示泛型的多个实例化
let summary1 = article.summarize("hello"); // 字符串类型
let summary2 = article.summarize(42); // 整数类型
println!("{}", summary1); // 输出: Rust Programming: "This is a hello summary."
println!("{}", summary2); // 输出: Rust Programming: "This is a 42 summary."
}
-
Summarizable
trait 定义了一个泛型方法summarize
,允许它接受任何类型的输入。 - 通过泛型,
summarize
方法可以在不同类型的上下文中使用,允许我们在不同情况下调用该方法,且无需为每种类型单独定义方法实现。
泛型约束
泛型约束(trait bounds)确保传入的类型实现了特定的 trait 或满足其他条件,从而增强代码的安全性和可用性。
例子:
use std::fmt::Debug;
fn print_debug<T: Debug>(item: T) {
println!("{:?}", item);
}
fn main() {
print_debug(42); // 整数类型实现了 Debug trait
print_debug("Hello, world!"); // 字符串类型实现了 Debug trait
}
- 通过
T: Debug
,我们为泛型类型T
添加了约束,要求类型必须实现Debug
trait。 - 泛型约束确保只有满足条件的类型才能作为泛型参数传递给函数。
Rust 特设多态
Rust 不支持传统的函数重载(同名函数根据参数列表区分),而是通过 trait
来实现特设多态,可以为不同的类型定义相同的方法接口,但具体实现各不相同。这种设计强调类型安全和编译时检查,避免运行时错误。
例子:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
// 为 Circle 实现 Shape trait,计算圆形的面积
impl Shape for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
// 为 Rectangle 实现 Shape trait,计算矩形的面积
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
let circle = Circle { radius: 3.0 };
let rectangle = Rectangle { width: 4.0, height: 5.0 };
// 不同类型的行为实现
println!("Circle area: {}", circle.area());
println!("Rectangle area: {}", rectangle.area());
}
特设多态可以和参数多态组合使用,使得 area
方法能根据不同的诉求返回不同的类型,而不是返回固定的 f64
。
改造后的例子:
// 泛型 Shape trait,计算面积
trait Shape<T> {
fn area(&self) -> T;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
// 为 Circle 实现泛型 Shape trait,计算圆形的面积,返回 f64 类型
impl Shape<f64> for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
// 为 Rectangle 实现泛型 Shape trait,计算矩形的面积,返回 i32 类型
impl Shape<i32> for Rectangle {
fn area(&self) -> i32 {
(self.width * self.height) as i32
}
}
fn main() {
let circle = Circle { radius: 3.0 };
let rectangle = Rectangle { width: 4.0, height: 5.0 };
// 计算不同类型的面积
let circle_area: f64 = circle.area();
let rectangle_area: i32 = rectangle.area();
println!("Circle area: {}", circle_area);
println!("Rectangle area: {}", rectangle_area);
}
Rust 子类型多态
子类型多态 是面向对象编程中的一种多态形式,它允许子类型的实例在运行时被当作父类型的实例来使用。传统的面向对象语言(如 Java 或 C++)通常通过继承或接口实现子类型多态。然而,Rust 不直接支持传统的继承或接口机制,而是通过 trait object(dyn Trait
) 和 动态分派(dynamic dispatch
) 来实现类似的功能。
在 Rust 中,当使用 trait object 时,实际上是在创建一个指向实现了该 trait 的类型的引用或智能指针。Rust 在运行时会根据实际的类型查找并调用相应的方法实现,这个过程就是 动态分派。动态分派的实现依赖于虚表(vtable),它是一个包含指向方法实现的指针表。每个类型的 trait object 都持有一个指向虚表的指针,以此来决定具体的函数调用。所以,trait object 是动态分派的载体,动态分派通过虚表实现,而虚表由 trait object 在运行时维护。
例子:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
// 为 Circle 实现 Shape trait
impl Shape for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
// 为 Rectangle 实现 Shape trait
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn print_area(shape: &dyn Shape) {
println!("The area is: {}", shape.area());
}
fn main() {
let circle = Circle { radius: 3.0 };
let rectangle = Rectangle { width: 4.0, height: 5.0 };
// 使用 &dyn Shape 实现子类型多态
print_area(&circle);
print_area(&rectangle);
}
该例子类似的功能也可以通过参数多态和特设多态的组合来实现,其中 print_area
函数不再根据子类型多态(通过trait object
实现)来计算不同类型的面积,而是根据不同类型的输入(通过泛型和 trait
实现)来计算相应的面积。
改造后的例子:
// 定义泛型 Shape trait
trait Shape<T> {
fn area(&self) -> f64;
}
// 定义 Circle 和 Rectangle 结构体
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
// 为 Circle 实现 Shape trait,计算圆形的面积
impl Shape<Circle> for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
// 为 Rectangle 实现 Shape trait,计算矩形的面积
impl Shape<Rectangle> for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
// 定义一个通用的函数来打印面积
fn print_area<T: Shape<T>>(shape: &T) {
println!("The area is: {}", shape.area());
}
fn main() {
let circle = Circle { radius: 3.0 };
let rectangle = Rectangle { width: 4.0, height: 5.0 };
// 使用泛型方式调用 print_area 函数
print_area(&circle);
print_area(&rectangle);
}
这两种方式的本质区别:
- 子类型多态:通过 trait object 和动态分派,在运行时决定具体类型的实现,适用于类型在编译时未知的场景,但带有运行时性能开销。
- 参数多态和特设多态的组合:通过泛型和 trait 的组合,在编译时决定具体类型的实现,适用于类型在编译时已知的场景,并且性能优越。
于是问题来了,类型在编译时未知的场景有什么特征?
为了回答这个问题,我们先看一下上面例子中的print_area
函数:不管是子类型多态,还是参数多态和特设多态的组合,print_area
函数均根据不同的子类型来计算相应的面积。
如果将print_area
函数改成print_areas
,输入类型变成Vec<Box<dyn Shape>>,该例子就会变为:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
// 为 Circle 实现 Shape trait
impl Shape for Circle {
fn area(&self) -> f64 {
3.14 * self.radius * self.radius
}
}
// 为 Rectangle 实现 Shape trait
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
// 修改后的 print_areas 函数,接受一个 Vec<Box<dyn Shape>>
fn print_areas(shapes: Vec<Box<dyn Shape>>) {
for shape in shapes {
println!("The area is: {}", shape.area());
}
}
fn main() {
let circle = Circle { radius: 3.0 };
let rectangle = Rectangle { width: 4.0, height: 5.0 };
// 将 Circle 和 Rectangle 包装到 Box 中,存入 Vec
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(circle),
Box::new(rectangle),
];
// 使用 print_areas 打印所有形状的面积
print_areas(shapes);
}
在编译print_area
函数时,编译器无法确定调用哪个类型的area
方法,因为 trait object 只是一个抽象类型。只有在运行时,Rust 才会根据具体类型(例如 Circle 或 Rectangle)查找正确的函数实现并调用它。
在 main
函数中,虽然我们知道 circle 和 rectangle 是 Circle 和 Rectangle 类型的实例,但 Rust 在print_area
函数中调用 area 方法时,并不会直接通过编译时的类型推导来选择方法,而是依赖动态分派。
小结
本文深入探讨了多态这一核心概念,并详细分析了在 Rust 中实现多态的三种主要方式及其组合应用的可能性:
- 参数多态:通过泛型编写通用代码,支持处理任意类型的数据,无需为每种类型单独实现,显著减少重复代码。
- 特设多态:利用 trait 为不同类型定义特定行为,灵活扩展功能,适应多样化需求。
- 子类型多态:通过 trait object 和动态分派,在运行时根据具体类型动态决定方法调用,相对于编译时多态,提供了更强的动态适应能力。
- 这三种方式既可单独应用,也能组合应用,有效应对复杂场景下的多态诉求。
Rust 的多态在确保类型安全的同时,兼顾了性能与扩展性。Rust 开发者可以根据具体业务场景灵活选择多态的应用方式,从而有效提升代码的复用性、可读性和可维护性。