今天來聊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中並沒有嚴格意義上的繼承,多是用組合的形式。這也體現了「多組合,少繼承」的設計思想。
最後留個預告,這個坑還沒完,我們下次繼續往深處挖。