目录

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
}

在调用函数时,保证ab参数类型相同,就正常调用了,再不新增函数的情况下,兼容了很多不同的数据类型。

Trait

Trait(特性)是 Rust 中另一种体现多态的方式。类似 Java 语言中的 interface(接口)概念。

可以使用 Java 中的接口来理解 Trait(同样是对行为的抽象),但是 Trait 的功能远远比 interface 强大,Trait 抽象能力更加广泛,不仅限于传统的对象。

例如下面例子中的SomethingTrait,DogCatBirdi32都实现了SomethingTrait 的sound()方法,其中i32是标准类型,Bird是枚举类型,DogCat自定义类型。

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

定义SomethingTrait,对象DogCat分别实现了Something特性的sound()方法。通过定义一个特性,我们可以编写能够处理任何实现了该特性的类型的代码。这就意味着我们可以对不同的类型调用相同的方法,只要他们都实现了同一个特性。这就是 Rust 中实现静态多态的主要方式。

Trait Objects 实现动态分发,当不确定具体类型时,只知道它实现了某个特性时,编译器在运行时通过查找虚方法表(vtable)中具体的struct的方法实现来调用,例如上面的列子,vec中具体存了什么类型,编译器并不知道,只有在程序运行时才知道(vec上的数据保存在堆上),那我们通过动态分发,执行Something特性的sound方法。

动态分发与静态分发,性能肯定是静态分发好,动态分发的优势在于灵活。