天天看點

Rust中的泛型

泛型程式設計是程式設計語言的一種風格或範式。泛型允許程式員在強類型程式設計語言中編寫代碼時使用一些以後才指定的類型,在執行個體化時作為參數指明這些類型。泛型程式設計的中心思想是從攜帶類型資訊的具體的算法中抽象出來,得到一種可以與不同的資料類型表示相結合的算法,進而生成各種有用的軟體。泛型程式設計是一種軟體工程中的解耦方法,很多時候,我們的算法并不依賴某種特定的具體類型,通過這種方法,我們就可以将“類型”從算法和資料結構的具體示例中抽象出來。

泛型作為函數參數的類型

考慮以下問題:編寫一個函數,這個函數接收兩個數字,然後傳回較大的那個數字。

fn largest(a: u32, b: u32) -> u32 {
    if a > b {
        a
    } else {
        b
    }
}
           

這個函數能工作,但它隻能比較兩個 u32 類型數字的大小。現在除了想比較兩個 u32 外,還想比較兩個 f32。有一種可以行的辦法,我們可以定義多個 largest 函數,讓它們分别叫做

largest_u32

largest_f32

… 這能正常工作,但不太美觀。我們可以使用泛型文法對上述代碼進行修改:

fn largest<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    println!("{}", largest::<u32>(1, 2));
    println!("{}", largest::<f32>(1.0, 2.1));
}
           

其中,

std::cmp::PartialOrd

被稱作泛型綁定,在之後的課程中我們會對此進行解釋。

結構體中的泛型

我們還可以使用泛型文法定義結構體,結構體中的字段可以使用泛型類型參數。下面的代碼展示了使用

Point<T>

結構來儲存任何類型的 x 和 y 坐标值。

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}
           

上述代碼建立了一個 x 和 y 都是同一類型的 Point 結構體,但同時一個結構體中也可以包含多個不同的泛型參數:

struct Point<T, U> {
    x: T,
    y: T,
    z: U,
}

fn main() {
    let integer = Point { x: 5, y: 10, z: 15.0 };
    let float = Point { x: 1.0, y: 4.0, z: 8 };
}
           

但是要注意,雖然一個結構體中可以包含任意多的泛型參數,但我仍然建議拆分結構體以使得一個結構體中隻使用一個泛型參數。過多的泛型參數會使得閱讀代碼的人難以閱讀。

結構體泛型的實作

我們可以在帶泛型的結構體上實作方法,它的文法與普通結構體方法相差不大,隻是要注意在它們的定義中加上泛型類型:

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}
           

我們也可以在某種具體類型上實作某種方法,例如下面的方法将隻在

Point<f32>

有效。

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}
           

使用traits定義共同的行為

某一類資料可能含有一些共同的行為:例如它們能被顯示在螢幕上,或者能互相之間比較大小。我們将這種共同的行為稱作 Traits。我們使用标準庫

std::fmt::Display

這個 traits 舉例,這個 traits 實作了在

Formatter

中使用空白格式

{}

的功能。

pub trait Display {
    pub fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}
           
use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

let origin = Point { x: 0, y: 0 };

assert_eq!(format!("The origin is: {}", origin), "The origin is: (0, 0)");
           

使用 Traits 作為參數類型

在知道如何定義和實作 Traits 後,我們就可以探索如何使用 Traits 來定義接受許多不同類型的函數。這一切都與 Java 中的接口概念類似,也就是所謂的鴨子類型。事實上它們的使用場景也基本上是類似的。

我們定義一個

display

函數,它接收一個實作了 Display Traits 的參數 item。

pub fn display(item: &impl std::fmt::Display) {
    println!("My display item is {}", item);
}
           

item 的參數類型是

impl std::fmt::Display

而不是某個具體的類型(例如 Point),這樣,任何實作了 Display Traits 的資料類型都可以作為參數傳入該函數。

自動派生

Rust 編譯器可以自動為我們的結構體實作一些 Traits,這種自動化技術被稱作派生。例如,在編寫代碼的過程中最常見的一個需求就是将結構體輸出的螢幕上,除了使用上節課提到的手工實作的 Display,也可以采用自動派生技術讓 Rust 編譯器自動幫你添加代碼。

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("{:?}", p);
}
           
#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    println!("{}", p1 == p2);
}
           

繼續閱讀