Rust入坑指南:海納百川

今天來聊Rust中兩個重要的概念:泛型和trait。很多編程語言都支持泛型,Rust也不例外,相信大家對泛型也都比較熟悉,它可以表示任意一種數據類型。trait同樣不是Rust所特有的特性,它借鑑於Haskell中的Typeclass。簡單來講,Rust中的trait就是對類型行爲的抽象,你可以把它理解爲Java中的接口。

泛型

在前面的文章中,我們其實已經提及了一些泛型類型。例如Option、Vec和Result<T, E>。泛型可以在函數、數據結構、Enum和方法中進行定義。在Rust中,我們習慣使用T作爲通用的類型名稱,當然也可以是其他名稱,只不過習慣上優先使用T(Type)來表示。它可以幫我們消除一些重複代碼,例如實現邏輯相同但參數類型不同的兩個函數,我們就可以通過泛型技術將其進行合併。下面我們分別演示泛型的幾種定義。

在函數中定義

泛型在函數的定義中,可以是參數,也可以是返回值。前提是必須要在函數名的後面加上。

fn largest<T>(list: &[T]) -> T {

在數據結構中定義

如果數據結構中某個字段可以接收任意數據類型,那麼我們可以把這個字段的類型定義爲T,同樣的,爲了讓編譯器認識這個T,我們需要在結構體名稱後邊標識一下。

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

上面的例子中,x和y都是可以接受任意類型,但是,它們兩個的類型必須相同,如果傳入的類型不同,編譯器仍然會報錯。那如果想要讓x和y能夠接受不同的類型應該怎麼辦呢?其實也很簡單,我們定義兩種不同的泛型就好了。

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

在Enum中定義

在Enum中定義泛型我們已經接觸過比較多了,最常見的例子就是Option和Result<T, E>。其定義方法也和在數據結構中的定義方法類似

enum Result<T, E> {
    Ok(T),
    Err(E),
}

在方法中定義

我們在實現定義了泛型的數據結構或Enum時,方法中也可以定義泛型。例如我們對剛剛定義的Point進行實現。

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

可以看到,我們的方法返回值的類型是T的引用,爲了讓編譯器識別T,我們必須要在impl後面加上<T>

另外,我們在對結構體進行實現時,也可以實現指定的類型,這樣就不需要在impl後面加標識了。

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

瞭解了泛型的幾種定義之後,你有沒有想過一個問題:Rust中使用泛型會對程序運行時的性能造成不良影響嗎?答案是不會,因爲Rust對於泛型的處理都是在編譯階段進行的,對於我們定義的泛型,Rust編譯器會對其進行單一化處理,也就是說,我們定義一個具有泛型的函數(或者其他什麼的),Rust會根據需要將其編譯爲具有具體類型的函數。

let integer = Some(5);
let float = Some(5.0);

例如我們的代碼使用了這兩種類型的Option,那麼Rust編譯器就會在編譯階段生成兩個指定具體類型的Option。

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

這樣我們在運行階段直接使用對應的Option就可以了,而不需要再進行額外複雜的操作。所以,如果我們泛型定義並使用的範圍很大,也不會對運行時性能造成影響,受影響的只有編譯後程序包的大小。

Trait

Trait可以說是Rust的靈魂,Rust中所有的抽象都是依靠Trait來實現的。

我們先來看看如何定義一個Trait。

pub trait Summary {
    fn summarize(&self) -> String;
}

定義trait使用了關鍵字trait,後面跟着trait的名稱。其內容是trait的「行爲」,也就是一個個函數。但是這裏的函數沒有實現,而是直接以;結尾。不過這這並不是必須的,Rust也支持下面這種寫法:

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

對於這樣的寫法,它表示summarize函數的默認實現。

Trait的實現

上面是一種默認實現,接下來我們介紹一下在Rust中,對一個Trait的常規實現。Trait的實現是需要針對結構體的,即我們要寫明是哪個結構體的哪種行爲。

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

上述代碼中,我們分別定義了結構體NewArticle和Tweet,然後爲它們實現了trait,定義了summarize函數對應的邏輯。

作爲參數的Trait

此外,trait還可以作爲函數的參數,也就是需要傳入一個實現了對應trait的結構體的實例。

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

作參數時,我們需要使用impl關鍵字來定義參數類型。

Rust還提供了另一種語法糖來,即Trait限定,我們可以使用泛型約束的語法來限定Trait參數。

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

如上述代碼,我們可以通過Trait來限定泛型T的範圍。這樣的語法糖可以在多個參數的函數中幫助我們簡化代碼。下面兩行代碼就有比較明顯的對比

pub fn notify(item1: impl Summary, item2: impl Summary) {

pub fn notify<T: Summary>(item1: T, item2: T) {

如果某個參數有多個trait限定,就可以使用+來表示

pub fn notify<T: Summary + Display>(item: T) {

如果我們有更多的參數,並且有每個參數都有多個trait限定,及時我們使用了上面這種語法糖,代碼仍然有些繁雜,會降低可讀性。所以Rust又爲我們提供了where關鍵字。

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

它幫助我們在函數定義的最後寫一個trait限定列表,這樣可以使代碼的可讀性更高。

Trait作爲返回值

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    }
}

Trait作爲返回值類型,和作爲參數類似,只需要在定義返回類型時使用impl Trait

總結

本文我們簡單介紹了泛型和Trait,包括它們的定義和使用方法。泛型主要是針對數據類型的一種抽象,而Trait則是對數據類型行爲的一種抽象,Rust中並沒有嚴格意義上的繼承,多是用組合的形式。這也體現了「多組合,少繼承」的設計思想。

最後留個預告,這個坑還沒完,我們下次繼續往深處挖。

發佈了141 篇原創文章 · 獲贊 375 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章