Rust 的三種多態
如果你已經編寫 Rust 一段時間了,那麼多態可能對你來說並不新鮮!但我還是希望這篇總結能在實踐中派上用場。不管怎樣,這對我很有用。
當你編寫的代碼需要處理幾種不同類型的值,而事先不知道它們是什麼時,Rust 對你的要求比很多語言都要多一點。當然,只要代碼在運行時能夠正常工作,動態語言就允許您傳入任何內容。Java/ c# 會要求一個接口或超類,鴨子類型的語言,如 Go 或 TypeScript,需要一些結構體類型,具有一組特定屬性的對象類型。
Rust 是不同的,在 Rust 中主要有三種處理這種情況的方法,每一種都有自己的優點和缺點。
問題
我們來看一個典型的多態問題,形狀問題。
Shape
|-Rectangle
|-Triangle
|-Circle
我們定義 perimeter() 和 area() 函數來求各形狀的周長和麪積,並且可以編寫使用這些函數的代碼,而不需要關心在給定時間查看哪個特定形狀。
Enums
// Data
enum Shape {
Rectangle { width: f32, height: f32 },
Triangle { side: f32 },
Circle { radius: f32 },
}
impl Shape {
pub fn perimeter(&self) -> f32 {
match self {
Shape::Rectangle { width, height } => width * 2.0 + height * 2.0,
Shape::Triangle { side } => side * 3.0,
Shape::Circle { radius } => radius * 2.0 * std::f32::consts::PI
}
}
pub fn area(&self) -> f32 {
match self {
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { side } => side * 0.5 * 3.0_f32.sqrt() / 2.0 * side,
Shape::Circle { radius } => radius * radius * std::f32::consts::PI
}
}
}
// Usage
fn print_area(shape: Shape) {
println!("{}", shape.area());
}
fn print_perimeters(shapes: Vec<Shape>) {
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
Rust 中的枚舉是一種數據結構,可以用來表示幾種不同的形狀之一。這些不同的形狀將存儲在內存中的同一個槽中 (槽的大小取其中最大的內存值)。
這是在 Rust 中實現多態性最直接的方法,它的優點是:
-
結構化的數據是內聯的 (不需要跟隨對其他內存位置的引用來查找它)。這裏最重要的一點是,集合中的枚舉在內存中 “彼此相鄰”,因此檢索它們所需的查找更少。這在對性能要求嚴格的場景中非常有用。
-
集合可以從它的元素中獲取不同的枚舉變體。
-
可以更容易地使用原始數據。
然而,它也有一些缺點:
-
如果不同變體的大小差異很大,可能會浪費一些內存。在某些變體中是一個大的集合,它可能無論如何都存在於堆中,而不是內聯。
-
更重要的一點是,lib 中的枚舉不能被該庫的用戶擴展。在枚舉定義好,它就是固定的。
Traits
// Data
trait Shape {
fn perimeter(&self) -> f32;
fn area(&self) -> f32;
}
struct Rectangle { pub width: f32, pub height: f32 }
struct Triangle { pub side: f32 }
struct Circle { pub radius: f32 }
impl Shape for Rectangle {
fn perimeter(&self) -> f32 {
self.width * 2.0 + self.height * 2.0
}
fn area(&self) -> f32 {
self.width * self.height
}
}
impl Shape for Triangle {
fn perimeter(&self) -> f32 {
self.side * 3.0
}
fn area(&self) -> f32 {
self.side * 0.5 * 3.0_f32.sqrt() / 2.0 * self.side
}
}
impl Shape for Circle {
fn perimeter(&self) -> f32 {
self.radius * 2.0 * std::f32::consts::PI
}
fn area(&self) -> f32 {
self.radius * self.radius * std::f32::consts::PI
}
}
trait 是 Rust 中另一個重要的多態概念。它們可以被認爲類似其他語言的接口或協議:它們指定一組必須實現的方法,任意的 struct 都可以實現它,這些 struct 可以在期望 trait 的地方使用。
它們相對於枚舉的一個主要優勢是,你可以從 crates 中導入一個 trait,爲你自己的 struct 實現它,然後將該 struct 傳遞給需要該 trait 的 crate 中。
有一個缺點,沒有辦法找出你正在使用的是哪個變體,並得到它的屬性。沒有 instanceof,沒有 as 類型轉換,你只能通過實際的 trait 方法來處理該值。
泛型 Trait (靜態分發)
// Usage
fn print_area<S: Shape>(shape: S) {
println!("{}", shape.area());
}
fn print_perimeters<S: Shape>(shapes: Vec<S>) { // !
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
Rust trait 可以用來約束泛型函數 (或泛型結構) 中的類型參數。我們可以說“S 必須是一個實現 Shape 的結構體”,這就給了我們在相關代碼中調用 trait 方法的權限。
與枚舉一樣,數據的大小在編譯時就已經知道了 (Rust 爲傳遞給它的每個具體類型實現不同的函數副本)。
不過,與枚舉不同的是,這阻止我們在同一泛型代碼中同時使用多個變量。例如:
fn main() {
let rectangle = Rectangle { width: 1.0, height: 2.0 };
let circle = Circle { radius: 1.0 };
print_area(rectangle); // ✅
print_area(circle); // ✅
print_perimeters(vec![ rectangle, circle ]); // compiler error!
}
這行不通,因爲 Vec 需要單一具體類型。我們可以使用 Vec 或 Vec,但不能同時使用。我們也不能只使用 Vec,因爲 Shape 在內存中沒有固定的大小。這只是一份接口契約,這就引出了……
Trait Object (動態分發)
// Usage
fn print_area(shape: &dyn Shape) {
println!("{}", shape.area());
}
fn print_perimeters(shapes: Vec<&dyn Shape>) {
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
在 Rust 語法中,&Foo 是對結構 Foo 的引用,而 & dyn Bar 是對實現某種特徵 Bar 的結構的引用。trait 沒有固定的大小,但是指針有固定的大小。
fn main() {
let rectangle = Rectangle { width: 1.0, height: 2.0 };
let circle = Circle { radius: 1.0 };
print_area(&rectangle); // ✅
print_area(&circle); // ✅
print_perimeters(vec![ &rectangle, &circle ]); // ✅
}
這裏我們可以混合匹配結構體,因爲它們的所有數據都在指針之後,指針有一個已知的大小,集合可以用它來分配內存。
需要注意的是:動態分發本身涉及到在虛表中查找所需的方法。通常,編譯器會提前知道方法代碼的確切內存位置,並可以硬編碼該地址。但使用動態分發,它無法提前知道它有什麼類型的結構,所以當代碼實際運行時,需要做一些額外的工作來找出它的方法在哪裏。
本文翻譯自:
https://www.brandons.me/blog/polymorphism-in-rust
coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/kzo8-m9lQIKrtajFK-jqDA