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 中實現多態性最直接的方法,它的優點是:

然而,它也有一些缺點:

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