Rust 中的多态
以前写 Java 代码时就对多态的了解总感觉模模糊糊,抓不住重点,学习了 Rust 之后,两者相互对比,倒是对这个概念有了更深入的理解。
什么是多态
维基百科 上的解释,多态(polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。
常见的多态的表现方式
- 特设多态:函数重载,运算符重载。
- 参数多态:泛型函数,泛型编程。
- 子类型:虚函数,单一与动态分派,多分派,双分派。
在不同的语言中,多态的表现方式也有所不同,例如 Java 语言中,多态是通过接口(interface),函数重载来体现的,而 Rust 语言中,多态是通过 Trait(特性)来体现的。
Rust 中的多态
泛型
在 Rust 中并不能使用函数重载的方式来体现多态性,例如下面代码:
fn add(a: u32, b: u32) -> u32 {
a + b
}
fn add(a: u8, b: u8) -> u8 {
a + b
}
运行编译会报错
error[E0428]: the name `add` is defined multiple times
--> src/main.rs:24:1
|
20 | fn add(a: u32, b: u32) -> u32 {
| ----------------------------- previous definition of the value `add` here
...
24 | fn add(a: u8, b: u8) -> u8 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `add` redefined here
|
= note: `add` must be defined only once in the value namespace of this module
For more information about this error, try `rustc --explain E0428`.
在 Rust 中不允许有相同的函数名,即使参数不同,但是这种函数重载在 Java 语言中应用十分广泛。
上面的add
方法通过泛型就可以编译通过了,代码如下:
fn add<T:std::ops::Add<Output = T>>(a: T, b: T) -> T {
a + b
}
在调用函数时,保证a
,b
参数类型相同,就正常调用了,再不新增函数的情况下,兼容了很多不同的数据类型。
Trait
Trait(特性)是 Rust 中另一种体现多态的方式。类似 Java 语言中的 interface(接口)概念。
可以使用 Java 中的接口来理解 Trait(同样是对行为的抽象),但是 Trait 的功能远远比 interface 强大,Trait 抽象能力更加广泛,不仅限于传统的对象。
例如下面例子中的Something
Trait,Dog
,Cat
,Bird
,i32
都实现了Something
Trait 的sound()
方法,其中i32
是标准类型,Bird
是枚举类型,Dog
,Cat
自定义类型。
Rust Trait 并不会在意特定类型(这里与 Java 中的接口,只面向对象类型),只关心类型是否实现了某个特性。
trait Something {
fn sound(&self) -> String;
}
struct Dog;
struct Cat;
enum Bird {
Duck,
}
impl Something for i32 {
fn sound(&self) -> String {
"not sound!".to_string()
}
}
impl Something for Bird {
fn sound(&self) -> String {
match self {
Bird::Duck => "Quack!".to_string(),
}
}
}
impl Something for Dog {
fn sound(&self) -> String {
"Woof!".to_string()
}
}
impl Something for Cat {
fn sound(&self) -> String {
"Meow!".to_string()
}
}
// 使用静态分发,编译器在编译时决定调用哪个方法
fn make_sound<T: Something>(thing: &T) -> String {
thing.sound()
}
fn main() {
let dog = Dog;
let cat = Cat;
let duck = Bird::Duck;
let number = 42;
println!("Dog sound: {}", make_sound(&dog));
println!("Cat sound: {}", make_sound(&cat));
println!("Duck sound: {}", make_sound(&duck));
println!("Number sound: {}", make_sound(&number));
// 使用动态分发的方式调用方法
let animals: Vec<Box<dyn Something>> = vec![
Box::new(dog),
Box::new(cat),
Box::new(duck),
Box::new(number),
];
for animal in animals {
// 动态分发的方式调用方法
println!("Something sound: {}", animal.sound());
}
}
定义Something
Trait,对象Dog
,Cat
分别实现了Something
特性的sound()
方法。通过定义一个特性,我们可以编写能够处理任何实现了该特性的类型的代码。这就意味着我们可以对不同的类型调用相同的方法,只要他们都实现了同一个特性。这就是 Rust 中实现静态多态的主要方式。
Trait Objects 实现动态分发,当不确定具体类型时,只知道它实现了某个特性时,编译器在运行时通过查找虚方法表(vtable)中具体的struct
的方法实现来调用,例如上面的列子,vec
中具体存了什么类型,编译器并不知道,只有在程序运行时才知道(vec
上的数据保存在堆上),那我们通过动态分发,执行Something
特性的sound
方法。
动态分发与静态分发,性能肯定是静态分发好,动态分发的优势在于灵活。