Rust 的三種多型性
當您編寫的程式碼應該可以處理幾種不同型別的值,但事先不知道它們是什麼,不同語言處理方式不同:
- 動態語言就可以讓您傳入任何內容。
- Java/C# 會要求一個介面或一個超類。
- Duck型別的語言,如 Go 或 TypeScript,需要一些結構型別:例如,具有一組特定屬性的物件型別。
在 Rust 中,有三種主要的方法來處理這種情況,每種方法都有自己的優點和缺點。
假設我們需要表示形狀,這是一個經典的多型問題:
Shape |-Rectangle |-Triangle |-Circle |
我們希望以這樣一種方式來表示這些,即它們每個都暴露它們的perimeter() 和area(),並且可以編寫程式碼來處理這些屬性,而無需關心它在給定時間看到的特定形狀。
1. 列舉
// 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 中實現多型的最直接的方法,它具有一些關鍵優勢:
- 結構資料是內聯的(不必遵循對其他記憶體位置的引用來找到它)。這裡最重要的事情是它有助於快取區域性性:集合中的實體將在記憶體中“彼此相鄰”,因此必須進行更少的旅行來檢索它們。快取區域性性對於本文來說是一個太大的話題,但它在效能關鍵程式碼中很重要。
- 即使資料是內聯的,例如中的每個專案。一個集合可以採用與其鄰居不同的變體。正如我們將看到的,這不是給定的。
- 您可以更輕鬆地將它們用作原始資料;正如我們將看到的,其他方法只允許您透過方法呼叫處理混合值。對於某些用例來說,這可能是不必要的負擔。
但是,它們也有一些缺點:
- 如果不同變體的大小差異很大,可能會浪費一些記憶體。這通常並不重要,因為如果您正在儲存例如。某些變體中的大型集合,無論如何它可能存在於堆中,而不是內聯。但在某些情況下,這可能很重要。
- 更重要的是:庫中公開的列舉不能被該庫的使用者擴充套件。在定義列舉的地方,它是一成不變的:所有可能的變體都列在那個地方。對於某些用途,這可能會破壞交易。
2. 特性Trait
// 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 } } |
Traits 是 Rust 中另一個重要的多型概念。它們可以被認為是來自其他語言的介面或協議:它們指定了一組結構必須實現的方法,然後它們可以為任意結構實現,並且這些結構可以用於預期特徵的地方。
與列舉相比,它們的一個主要優勢是可以為其他地方的新結構實現該特徵——即使在不同的箱子中。您可以從 crate 匯入 trait,為您自己的結構實現它,然後將該結構傳遞給需要該 trait 的 crate 中的程式碼。這對於某些型別的庫來說可能是至關重要的。
還有一個巧妙的(如果是利基的)好處:您可以選擇編寫只接受特定變體的程式碼。使用列舉你不能這樣做(我希望你能!)。
一個缺點,這在其他語言中不會很明顯:無法透過特徵找出您正在使用的變體並獲取其其他屬性。
與大多數具有類似概念的語言不同,Rust 為我們提供了一個有趣的選擇,讓我們可以在如何使用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<Rectangle>或 Vec<Circle>,但不能同時擁有。
我們不能只擁有一個Vec<Shape>,因為Shape在記憶體中沒有固定的大小。這只是一份合同。
- 2b. 具有動態排程的特徵trait
// 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 struct 的引用,而&dyn Bar是對實現某些trait 的Bar struct 的引用。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 ]); // } |
我們可以在這裡混合和匹配結構,因為它們的所有資料都在指標後面,並且指標具有已知的大小,集合可用於分配記憶體。
那麼缺點是什麼?主要是,我們失去了快取區域性性。因為所有結構的資料都在指標後面,所以計算機必須到處跳來追蹤它。多次這樣做,這可能會開始對效能產生重大影響。
值得注意的是:動態排程本身涉及在查詢表中查詢所需的方法。通常編譯器會提前知道方法程式碼的確切記憶體位置,並且可以對該地址進行硬編碼。但是對於動態排程,它無法提前知道它具有什麼樣的結構,因此當程式碼實際執行時,需要做一些額外的工作來弄清楚它並查詢其方法所在的位置。
最後:在實踐中,如果某個結構擁有一個僅其特徵已知的值,則您可能必須將該值放入一個Box中,這意味著進行堆分配,而該分配/解除分配本身可能會很昂貴。
原文點選標題
相關文章
- Rust 中的位元組序、API 設計和多型性 - JimmyRustAPI多型
- 多型性多型
- Rust的安全性和穩健型Rust
- rust學習三、基本型別Rust型別
- 多型性概述多型
- 建立多種型別的流型別
- 精讀《React 的多型性》React多型
- 多型性的論述 (轉)多型
- C# 多型性C#多型
- C# 多型性C#多型
- c#多型性C#多型
- C++多型性C++多型
- oracle的三種型別的程式Oracle型別
- java多型性淺析Java多型
- MRP area的三種型別型別
- 三種輔導型別型別
- DOCTYPE宣告三種型別型別
- c#——泛型的多種應用C#泛型
- 資料中心代理的三種型別型別
- SAP QM 三種型別的Physical Sample型別
- 06-redis的三種特殊型別Redis型別
- 資料更改事件的三種型別事件型別
- 多型性----vptr----vtable多型
- 多型體驗,和探索爺爺類指標的多型性多型指標
- Flutter Key的原理和使用(三) LocalKey的三種型別Flutter型別
- 常見的三種沉浸式投影型別型別
- c語言中陣列的三種型別C語言陣列型別
- 5G訊息的三種型別型別
- Redis 中三種特殊的資料型別Redis資料型別
- Redis 三種特殊資料型別Redis資料型別
- 多型 案例三 (電腦組裝)多型
- 分析三種型別的物聯網平臺型別
- 三種型別的物聯網平臺分析型別
- 常見的三種HTTP代理服務型別HTTP型別
- PHP中資料型別轉換的三種方式PHP資料型別
- XSD中自定義型別的三種方式型別
- Oracle三種集合資料型別的比較Oracle資料型別
- c#多型性測試小例C#多型