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