聊聊Rust的多態(tài)

引言

多態(tài)(Polymorphism)是面向?qū)ο缶幊毯皖愋拖到y(tǒng)中的核心概念,它是指在使用相同的接口時,不同類型的對象,會采用不同的實現(xiàn)。

根據(jù)類型系統(tǒng)的不同,多態(tài)的實現(xiàn)方式也有所差異:

  • 在動態(tài)類型系統(tǒng)中,多態(tài)主要通過鴨子類型(Duck Typing)實現(xiàn)。
  • 在靜態(tài)類型系統(tǒng)中,多態(tài)則可以通過參數(shù)多態(tài)(Parametric Polymorphism)、特設(shè)多態(tài)(Ad-hoc Polymorphism)和子類型多態(tài)(Subtype Polymorphism)來實現(xiàn)。

對于 Python、Ruby 和 JavaScript 等動態(tài)類型語言來說,變量不綁定到具體的類型,而是綁定到具體的對象。如果一個對象具有某種方法或?qū)傩?,并且這些方法或?qū)傩钥梢员徽_調(diào)用,那么它就可以被認(rèn)為是實現(xiàn)了某種“接口”或“能力”。鴨子類型的核心思想可以用這句經(jīng)典的話概括:“If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck”,即:如果某個對象“看起來像鴨子,游泳像鴨子,叫聲也像鴨子”,那我們就可以認(rèn)為它是一只鴨子,而不需要關(guān)心它的具體類型。

對于 Rust、C++、Java 和 Go 等靜態(tài)類型語言來說,變量不綁定到具體的對象,而是綁定到具體的類型。這意味著在編譯期,編譯器會對變量的類型進行嚴(yán)格檢查,確保類型匹配和方法調(diào)用的正確性。在這些語言中,多態(tài)性是通過顯式的類型約束或繼承機制來實現(xiàn)的,程序員需要明確地定義對象的行為和能力,而不能依賴動態(tài)檢查。這類語言強調(diào)類型的靜態(tài)綁定,其參數(shù)多態(tài)、特設(shè)多態(tài)和子類型多態(tài)的支持方式如下:

語言 參數(shù)多態(tài) 特設(shè)多態(tài) 子類型多態(tài)
Rust 泛型(<T> trait 動態(tài)分派(trait object
C++ 泛型(模板) 函數(shù)重載、運算符重載 虛函數(shù)(virtual)
Java 泛型 方法重載 繼承和接口
Go 泛型(1.18 及以后) 無直接支持(可模擬) 接口

說明如下:

  • 參數(shù)多態(tài):指代碼能夠處理不同類型的參數(shù),而不需要事先指定具體的類型。通常通過 泛型 來實現(xiàn),使得函數(shù)、類或接口能夠接受不同類型的輸入而不犧牲類型安全。例如,通過泛型,我們可以編寫通用代碼來處理任意類型的數(shù)據(jù),而無需為每個類型編寫不同的實現(xiàn)。泛型使得我們能夠創(chuàng)建能夠適用于多種數(shù)據(jù)類型的函數(shù)和類,減少了重復(fù)代碼。
  • 特設(shè)多態(tài):指同一種行為有多個不同的實現(xiàn)。比如,加法操作可以有不同的實現(xiàn):對于整數(shù)數(shù)據(jù)類型,它是數(shù)學(xué)加法;對于字符串,它是拼接;對于自定義類型,它可能是矩陣加法等。通常,特設(shè)多態(tài)通過函數(shù)重載運算符重載實現(xiàn),這些技術(shù)允許使用相同的名稱或符號(如 +)進行不同類型的操作。在不同的語言中,特設(shè)多態(tài)可以通過不同的機制實現(xiàn),如 C++ 中的函數(shù)重載和運算符重載,Java 中的方法重載,或 Rust 中的 trait 實現(xiàn)。
  • 子類型多態(tài):指在運行時子類的對象可以被當(dāng)作父類的引用來使用。這使得父類的引用能夠指向任何派生自該父類的對象,從而能夠?qū)崿F(xiàn)靈活的行為擴展和替換。子類型多態(tài)通常通過繼承接口動態(tài)分派實現(xiàn),在調(diào)用方法時,系統(tǒng)根據(jù)實際對象的類型(而非引用類型)決定調(diào)用哪個方法。這類多態(tài)是面向?qū)ο缶幊讨蟹浅V匾奶匦?,通常通過接口、抽象類和虛方法等機制來實現(xiàn)。

Rust 參數(shù)多態(tài)

在 Rust 中,參數(shù)多態(tài) 是通過 泛型 來實現(xiàn)的。泛型允許函數(shù)、結(jié)構(gòu)體、枚舉和 trait 處理不同類型的數(shù)據(jù),而不需要為每種類型單獨編寫代碼。它使得代碼更加通用和靈活,可以在編譯時根據(jù)傳入的類型進行具體化,而無需運行時的類型檢查。

函數(shù)中的泛型

函數(shù)中的泛型可以讓我們編寫通用的函數(shù),不同的類型可以作為參數(shù)傳入,而不需要為每種類型單獨編寫不同版本的函數(shù)。

例子:

fn multiply<T>(a: T, b: T) -> T {
    a * b
}

fn main() {
    let int_result = multiply(2, 3);          // 整數(shù)類型
    let float_result = multiply(2.5, 3.0);    // 浮點數(shù)類型

    println!("Integer result: {}", int_result);
    println!("Float result: {}", float_result);
}
  • multiply 是泛型函數(shù),輸入兩個類型為 T 的參數(shù) a 和 b,并返回類型為 T 的值。
  • multiply 在編譯時根據(jù)實際傳入的類型進行替換,可以接受任何類型的參數(shù),只要該類型支持 * 操作。

對于泛型函數(shù),Rust 會進行單態(tài)化(Monomorphization)處理,也就是在編譯時,把所有用到的泛型函數(shù)的泛型參數(shù)展開,生成若干個函數(shù)。

單態(tài)化的優(yōu)勢:泛型函數(shù)的調(diào)用是靜態(tài)分派(static dispatch),在編譯時就一一對應(yīng),既保留了多態(tài)的靈活性,又沒有任何效率的損失,和普通函數(shù)調(diào)用一樣高效。

然而,單態(tài)化也有很明顯的劣勢:

  • 編譯速度很慢,一個泛型函數(shù),編譯器需要找到所有用到的不同類型,一個個編譯,所以 Rust 編譯代碼的速度總被人吐槽,這和單態(tài)化脫不開干系。
  • 編出來的二進制會比較大,因為泛型函數(shù)的二進制代碼實際存在 N 份。
  • 代碼以二進制分發(fā)會損失泛型的信息,因為單態(tài)化之后,原本的泛型信息被丟棄了。

結(jié)構(gòu)體中的泛型

結(jié)構(gòu)體中的泛型允許你定義可以處理多種類型的結(jié)構(gòu)體,從而提高代碼復(fù)用性。

例子:

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() {
    // 創(chuàng)建第一個 Pair 實例,類型為 (i32, &str)
    let pair1 = Pair::new(1, "hello");
    println!("Pair 1 - First: {}, Second: {}", pair1.first(), pair1.second());

    // 創(chuàng)建第二個 Pair 實例,類型為 (f64, bool)
    let pair2 = Pair::new(3.14, true);
    println!("Pair 2 - First: {}, Second: {}", pair2.first(), pair2.second());
}

  • Pair 是一個帶有泛型的結(jié)構(gòu)體,它可以存儲任何類型的兩個值,分別由 TU 來表示。
  • 通過泛型,我們可以創(chuàng)建多個類型組合的結(jié)構(gòu)體實例。

我們知道,結(jié)構(gòu)體內(nèi)部如果有借用的字段,需要顯式地標(biāo)注生命周期。其實在 Rust 里,生命周期標(biāo)注也是泛型的一部分,一個生命周期 '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 枚舉可以容納任何類型的數(shù)據(jù)。

trait 中的泛型

trait 中的泛型使得我們可以定義通用的行為,并為多種類型提供實現(xiàn)。

例子:

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 來調(diào)用 summarize 方法,展示泛型的多個實例化
    let summary1 = article.summarize("hello");  // 字符串類型
    let summary2 = article.summarize(42);       // 整數(shù)類型

    println!("{}", summary1); // 輸出: Rust Programming: "This is a hello summary."
    println!("{}", summary2); // 輸出: Rust Programming: "This is a 42 summary."
}
  • Summarizable trait 定義了一個泛型方法 summarize,允許它接受任何類型的輸入。
  • 通過泛型,summarize 方法可以在不同類型的上下文中使用,允許我們在不同情況下調(diào)用該方法,且無需為每種類型單獨定義方法實現(xiàn)。

泛型約束

泛型約束(trait bounds)確保傳入的類型實現(xiàn)了特定的 trait 或滿足其他條件,從而增強代碼的安全性和可用性。

例子:

use std::fmt::Debug;

fn print_debug<T: Debug>(item: T) {
    println!("{:?}", item);
}

fn main() {
    print_debug(42);            // 整數(shù)類型實現(xiàn)了 Debug trait
    print_debug("Hello, world!"); // 字符串類型實現(xiàn)了 Debug trait
}
  • 通過 T: Debug,我們?yōu)榉盒皖愋?T 添加了約束,要求類型必須實現(xiàn) Debug trait。
  • 泛型約束確保只有滿足條件的類型才能作為泛型參數(shù)傳遞給函數(shù)。

Rust 特設(shè)多態(tài)

Rust 不支持傳統(tǒng)的函數(shù)重載(同名函數(shù)根據(jù)參數(shù)列表區(qū)分),而是通過 trait 來實現(xiàn)特設(shè)多態(tài),可以為不同的類型定義相同的方法接口,但具體實現(xiàn)各不相同。這種設(shè)計強調(diào)類型安全和編譯時檢查,避免運行時錯誤。

例子:

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// 為 Circle 實現(xiàn) Shape trait,計算圓形的面積
impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

// 為 Rectangle 實現(xiàn) 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 };

    // 不同類型的行為實現(xiàn)
    println!("Circle area: {}", circle.area());
    println!("Rectangle area: {}", rectangle.area());
}

特設(shè)多態(tài)可以和參數(shù)多態(tài)組合使用,使得 area 方法能根據(jù)不同的訴求返回不同的類型,而不是返回固定的 f64。

改造后的例子:

// 泛型 Shape trait,計算面積
trait Shape<T> {
    fn area(&self) -> T;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// 為 Circle 實現(xiàn)泛型 Shape trait,計算圓形的面積,返回 f64 類型
impl Shape<f64> for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

// 為 Rectangle 實現(xiàn)泛型 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 子類型多態(tài)

子類型多態(tài) 是面向?qū)ο缶幊讨械囊环N多態(tài)形式,它允許子類型的實例在運行時被當(dāng)作父類型的實例來使用。傳統(tǒng)的面向?qū)ο笳Z言(如 Java 或 C++)通常通過繼承接口實現(xiàn)子類型多態(tài)。然而,Rust 不直接支持傳統(tǒng)的繼承或接口機制,而是通過 trait object(dyn Trait動態(tài)分派(dynamic dispatch 來實現(xiàn)類似的功能。

在 Rust 中,當(dāng)使用 trait object 時,實際上是在創(chuàng)建一個指向?qū)崿F(xiàn)了該 trait 的類型的引用或智能指針。Rust 在運行時會根據(jù)實際的類型查找并調(diào)用相應(yīng)的方法實現(xiàn),這個過程就是 動態(tài)分派。動態(tài)分派的實現(xiàn)依賴于虛表(vtable),它是一個包含指向方法實現(xiàn)的指針表。每個類型的 trait object 都持有一個指向虛表的指針,以此來決定具體的函數(shù)調(diào)用。所以,trait object 是動態(tài)分派的載體,動態(tài)分派通過虛表實現(xiàn),而虛表由 trait object 在運行時維護。

例子:

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// 為 Circle 實現(xiàn) Shape trait
impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

// 為 Rectangle 實現(xiàn) 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 實現(xiàn)子類型多態(tài)
    print_area(&circle);
    print_area(&rectangle);
}

該例子類似的功能也可以通過參數(shù)多態(tài)和特設(shè)多態(tài)的組合來實現(xiàn),其中 print_area函數(shù)不再根據(jù)子類型多態(tài)(通過trait object實現(xiàn))來計算不同類型的面積,而是根據(jù)不同類型的輸入(通過泛型和 trait實現(xiàn))來計算相應(yīng)的面積。

改造后的例子:

// 定義泛型 Shape trait
trait Shape<T> {
    fn area(&self) -> f64;
}

// 定義 Circle 和 Rectangle 結(jié)構(gòu)體
struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// 為 Circle 實現(xiàn) Shape trait,計算圓形的面積
impl Shape<Circle> for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

// 為 Rectangle 實現(xiàn) Shape trait,計算矩形的面積
impl Shape<Rectangle> for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

// 定義一個通用的函數(shù)來打印面積
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 };

    // 使用泛型方式調(diào)用 print_area 函數(shù)
    print_area(&circle);
    print_area(&rectangle);
}

這兩種方式的本質(zhì)區(qū)別

  • 子類型多態(tài):通過 trait object 和動態(tài)分派,在運行時決定具體類型的實現(xiàn),適用于類型在編譯時未知的場景,但帶有運行時性能開銷。
  • 參數(shù)多態(tài)和特設(shè)多態(tài)的組合:通過泛型和 trait 的組合,在編譯時決定具體類型的實現(xiàn),適用于類型在編譯時已知的場景,并且性能優(yōu)越。

于是問題來了,類型在編譯時未知的場景有什么特征

為了回答這個問題,我們先看一下上面例子中的print_area函數(shù):不管是子類型多態(tài),還是參數(shù)多態(tài)和特設(shè)多態(tài)的組合,print_area函數(shù)均根據(jù)不同的子類型來計算相應(yīng)的面積。

如果將print_area函數(shù)改成print_areas,輸入類型變成Vec<Box<dyn Shape>>,該例子就會變?yōu)椋?/p>

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

// 為 Circle 實現(xiàn) Shape trait
impl Shape for Circle {
    fn area(&self) -> f64 {
        3.14 * self.radius * self.radius
    }
}

// 為 Rectangle 實現(xiàn) Shape trait
impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

// 修改后的 print_areas 函數(shù),接受一個 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函數(shù)時,編譯器無法確定調(diào)用哪個類型的area方法,因為 trait object 只是一個抽象類型。只有在運行時,Rust 才會根據(jù)具體類型(例如 Circle 或 Rectangle)查找正確的函數(shù)實現(xiàn)并調(diào)用它。

main函數(shù)中,雖然我們知道 circle 和 rectangle 是 Circle 和 Rectangle 類型的實例,但 Rust 在print_area函數(shù)中調(diào)用 area 方法時,并不會直接通過編譯時的類型推導(dǎo)來選擇方法,而是依賴動態(tài)分派。

小結(jié)

本文深入探討了多態(tài)這一核心概念,并詳細(xì)分析了在 Rust 中實現(xiàn)多態(tài)的三種主要方式及其組合應(yīng)用的可能性:

  • 參數(shù)多態(tài):通過泛型編寫通用代碼,支持處理任意類型的數(shù)據(jù),無需為每種類型單獨實現(xiàn),顯著減少重復(fù)代碼。
  • 特設(shè)多態(tài):利用 trait 為不同類型定義特定行為,靈活擴展功能,適應(yīng)多樣化需求。
  • 子類型多態(tài):通過 trait object 和動態(tài)分派,在運行時根據(jù)具體類型動態(tài)決定方法調(diào)用,相對于編譯時多態(tài),提供了更強的動態(tài)適應(yīng)能力。
  • 這三種方式既可單獨應(yīng)用,也能組合應(yīng)用,有效應(yīng)對復(fù)雜場景下的多態(tài)訴求。

Rust 的多態(tài)在確保類型安全的同時,兼顧了性能與擴展性。Rust 開發(fā)者可以根據(jù)具體業(yè)務(wù)場景靈活選擇多態(tài)的應(yīng)用方式,從而有效提升代碼的復(fù)用性、可讀性和可維護性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容