1. 程式人生 > >Rust入坑指南:海納百川

Rust入坑指南:海納百川

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

泛型

在前面的文章中,我們其實已經提及了一些泛型型別。例如Option

在函式中定義

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

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

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中並沒有嚴格意義上的繼承,多是用組合的形式。這也體現了「多組合,少繼承」的設計思想。

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