今天來聊Rust中兩個重要的概念:泛型和trait。很多編程語言都支持泛型,Rust也不例外,相信大家對泛型也都比較熟悉,它可以表示任意一種數據類型。trait同樣不是Rust所特有的特性,它借鑒于Haskell中的Typeclass。簡單來講,Rust中的trait就是對類型行為的抽象,你可以把它理解為Java中的接口。
泛型
在前面的文章中,我們其實已經提及了一些泛型類型。例如Option<T>、Vec<T>和Result<T, E>。泛型可以在函數、數據結構、Enum和方法中進行定義。在Rust中,我們習慣使用T作為通用的類型名稱,當然也可以是其他名稱,只不過習慣上優先使用T(Type)來表示。它可以幫我們消除一些重復代碼,例如實現邏輯相同但參數類型不同的兩個函數,我們就可以通過泛型技術將其進行合并。下面我們分別演示泛型的幾種定義。
在函數中定義
泛型在函數的定義中,可以是參數,也可以是返回值。前提是必須要在函數名的后面加上<T>。
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<T>和Result<T, E>。其定義方法也和在數據結構中的定義方法類似
enum Result<T, E> {
Ok(T),
Err(E),
}
在方法中定義
我們在實現定義了泛型的數據結構或Enum時,方法中也可以定義泛型。例如我們對剛剛定義的Point<T>進行實現。
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中并沒有嚴格意義上的繼承,多是用組合的形式。這也體現了「多組合,少繼承」的設計思想。
最后留個預告,這個坑還沒完,我們下次繼續往深處挖。