引言
多態(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)體,它可以存儲任何類型的兩個值,分別由T和U來表示。 - 通過泛型,我們可以創(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."
}
-
Summarizabletrait 定義了一個泛型方法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)Debugtrait。 - 泛型約束確保只有滿足條件的類型才能作為泛型參數(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ù)用性、可讀性和可維護性。