聊聊Rust的多态

引言

多态(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 是一个带有泛型的结构体,它可以存储任何类型的两个值,分别由 TU 来表示。
  • 通过泛型,我们可以创建多个类型组合的结构体实例。

我们知道,结构体内部如果有借用的字段,需要显式地标注生命周期。其实在 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 开发者可以根据具体业务场景灵活选择多态的应用方式,从而有效提升代码的复用性、可读性和可维护性。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容