泛型是具體類型或其他屬性的抽象替代。在編寫代碼時,可以直接描述泛型的行為,或者它與其他泛型產生的聯系,而無須知曉它在編譯和運行代碼時采用的具體類型。
1、泛型數據類型:
們可以在聲明函數簽名或結構體等元素時使用泛型,并在隨后搭配不同的具體類型來使用這些元素。
(1)、在函數定義中:
當使用泛型來定義一個函數時,需要將泛型放置在函數簽名中通常用于指定參數和返回值類型的地方。以這種方式編寫的代碼更加靈活,并可以在不引入重復代碼的同時向函數調用者提供更多的功能。
當需要在函數簽名中使用類型參數時,也需要在使用前聲明這個類型參數的名稱。為了定義泛型版本的largest函數,類型名稱的聲明必須被放置在函數名與參數列表之間的一對尖括號<>
中,如下所示:
fn largest<T>(list: &[T]) -> T {}
即函數largest擁有泛型參數T,它接收一個名為list的T值切片作為參數,并返回一個同樣擁有類型T的值作為結果。
(2)、在結構體定義中:
可以使用<>語法來定義在一個或多個字段中使用泛型的結構體。示例:
struct Point<T> {x: T,y: T,
}
fn main() {let integer = Point { x: 5, y: 10 };let float = Point { x: 1.0, y: 4.0 };
}
在結構名后的一對尖括號中聲明泛型參數后,就可以在結構體定義中那些通常用于指定具體數據類型的位置使用泛型了。
在定義Point時僅使用了一個泛型,這個定義表明Point結構體對某個類型T是通用的。而無論具體的類型是什么,字段x與y都同時屬于這個類型。但是使用不同的值類型來創建Point實例,那么代碼是無法通過編譯的。示例:
struct Point<T> {x: T,y: T,
}
fn main() {let wont_work = Point { x: 5, y: 4.0 };
}
這段程序無法編譯通過。字段x和y必須是相同的類型,因為它們擁有相同的泛型T。
為了在保持泛型狀態的前提下,讓Point結構體中的x和y能夠被實例化為不同的類型,可以使用多個泛型參數。示例:
struct Point<T, U> {x: T,y: U,
}
fn main() {let both_integer = Point { x: 5, y: 10 };let both_float = Point { x: 1.0, y: 4.0 };let integer_and_float = Point { x: 5, y: 4.0 };
}
(3)、在枚舉定義中:
枚舉定義也可以在它們的變體中存放泛型數據。例如標準庫中提供的Option枚舉:
enum Option<T> {Some(T),None,
}
Option是一個擁有泛型T的枚舉。它擁有兩個變體:持有T類型值的Some變體,以及一個不持有任何值的None變體。Option被用來表示一個值可能存在的抽象概念。也正是因為Option使用了泛型,所以無論這個可能存在的值是什么類型,都可以通過Option來表達這一抽象。
枚舉同樣也可以使用多個泛型參數。例如的Result枚舉:
enum Result<T, E> {Ok(T),Err(E),
}
Result枚舉擁有兩個泛型:T和E。它也同樣擁有兩個變體:持有T類型值的Ok,以及一個持有E類型值的Err。這個定義使得Result枚舉可以很方便地被用在操作可能成功(返回某個T類型的值),也可能失敗(返回某個E類型的錯誤)的場景。
(4)、在方法定義中:
方法也可以在自己的定義中使用泛型。例如結構體Point實現了一個名為x的方法:
struct Point<T> {x: T,y: T,
}
impl<T> Point<T> {fn x(&self) -> &T {&self.x}
}
fn main() {let p = Point { x: 5, y: 10 };println!("p.x = {}", p.x());
}
在上面的代碼中,我們為結構體Point定義了一個名為x的方法,它會返回一個指向字段x中數據的引用。
注意,必須緊跟著impl關鍵字聲明T,以便能夠在實現方法時指定類型Point。通過在impl之后將T聲明為泛型,Rust能夠識別出Point尖括號內的類型是泛型而不是具體類型。
(5)、泛型代碼的性能問題:
Rust實現泛型的方式決定了使用泛型的代碼與使用具體類型的代碼相比不會有任何速度上的差異。
Rust會在編譯時執行泛型代碼的單態化(monomorphization)。單態化是一個在編譯期將泛型代碼轉換為特定代碼的過程,它會將所有使用過的具體類型填入泛型參數從而得到有具體類型的代碼。
在這個過程中,編譯器會尋找所有泛型代碼被調用過的地方,并基于該泛型代碼所使用的具體類型生成代碼。
2、trait:定義共享行為:
trait(特征)被用來向Rust編譯器描述某些特定類型擁有且能夠被其他類型共享的功能,它使我們可以以一種抽象的方式來定義共享特征。還可以使用trait約束泛型參數指定為實現了某些特定行為的類型。
(1)、定義trait:
類型的行為由該類型本身可供調用的方法組成。當在不同的類型上調用了相同的方法時,就稱這些類型共享了相同的行為。trait提供了一種將特定方法簽名組合起來的途徑,它定義了達成某種目的所必需的行為集合。示例:
pub trait Summary {fn summarize(&self) -> String;
}
這里,我們使用了trait關鍵字來聲明tait,緊隨關鍵字的是該trait的名字。在其后的花括號中,聲明了用于定義類型行為的方法簽名。在方法簽名后,省略了花括號及具體的實現,直接使用分號終結了當前的語句。任何想要實現這個trait的類型都需要為上述方法提供自定義行為。編譯器會確保每一個實現了Summary trait的類型都定義了與這個簽名完全一致的summarize方法。
一個trait可以包含多個方法:每個方法簽名占據單獨一行并以分號結尾。
(2)、為類型實現trait:
基于Summary 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)}
}
為類型實現trait與實現普通方法的步驟十分類似。它們的區別在于我們必須在impl關鍵字后提供我們想要實現的trait名,并緊接for關鍵字及當前的類型名。在impl代塊中,我們同樣需要填入trait中
的方法簽名。但在每個簽名的結尾不再使用分號,而是使用花括號并在其中編寫函數體來為這個特定類型實現該trait的方法所應具有的行為。
一旦實現了trait,我們便可以基于NewsArticle和Tweet的實例調用該trait的方法了,正如我們調用普通方法一樣。示例:
let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
注意,實現trait有一個限制:只有當trait或類型定義于我們的庫中時,我們才能為該類型實現對應的trait。
我們不能為外部類型實現外部trait。例如,我們不能在aggregator庫內為Vec實現Display trait,因為Display與Vec都被定義在標準庫中,而沒有定義在aggregator庫中。這個限制被稱為孤兒規則 (orphan rule)
,之所以這么命名是因為它的父類型沒有定義在當前庫中。這一規則也是程序一致性 (coherence)的組成部分,它確保了其他人所編寫的內容不會破壞到你的代碼,反之亦
然。如果沒有這條規則,那么兩個庫可以分別對相同的類型實現相同的trait,Rust將無法確定應該使用哪一個版本。
(3)、默認實現:
有些時候,為trait中的某些或所有方法都提供默認行為非常有用,它使我們無須為每一個類型的實現都提供自定義行為。當我們在為某個特定類型實現trait時,可以選擇保留或重載每個方法的默認行為。示例:
pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}
}
假如我們決定在NewsArticle的實例中使用這種默認實現而不是自定義實現,那么我們可以指定一個空的impl代碼塊:impl Summaryfor NewsArticle {}
。
為summarize提供一個默認實現并不會影響為Tweet實現Summary時所編寫的代碼。這是因為重載默認實現與實現trait方法的語法完全一致。
還可以在默認實現中調用相同trait中的其他方法,哪怕這些方法沒有默認實現。基于這一規則,trait可以在只需要實現一小部分方法的前提下,提供許多有用的功能。示例:
pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(Read more from {}...)", self.summarize_author())}
}
impl Summary for Tweet {fn summarize_author(&self) -> String {format!("@{}", self.username)}
}
let tweet = Tweet {username: String::from("horse_ebooks"),content: String::from("of course, as you probably already know, people"),reply: false,retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
這 段 代 碼 會 打 印 出 1 new tweet: (Read@horse_ebooks...)
。
(4)、使用trait作為參數:
前面我們為NewsArticle 與 Tweet 類 型 實 現 了Summary trait。我們可以定義一個notify函數來調用其item參數的summarize方法,這里的參數item可以是任何實現了Summary trait的類型。
pub fn notify(item: impl Summary) {println!("Breaking news! {}", item.summarize());
}
我們沒有為item參數指定具體的類型,而是使用了impl關鍵字及對應的trait名稱。這一參數可以接收任何實現了指定trait的類型。在notify的函數體內,我們可以調用來自Summary trait的任何方法,
當然也包括summarize。我們可以在調用notify時向其中傳入任意一個NewsArticle或Tweet實例。假設我們需要接收兩個都實現了Summary的參數,那么使用impl Trait的寫法如下所示:
pub fn notify(item1: impl Summary, item2: impl Summary) {}
假如notify函數需要在調用summarize方法的同時顯示格式化后的item,那么item就必須實現兩個不同的trait:Summary和Display。我們可以使用+語法做到這一點:
pub fn notify(item: impl Summary + Display) {}
(5)、返回實現了trait的類型:
同樣可以在返回值中使用impl 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,}
}
3、使用生命周期保證引用的有效性:
Rust的每個引用都有自己的生命周期(lifetime),它對應著引用保持有效性的作用域。在大多數時候,生命周期都是隱式且可以被推導出來的,就如同大部分時候類型也是可以被推導的一樣。當出現了多個可能的類型時,就必須手動聲明類型。
(1)、使用生命周期來避免懸垂引用:
生命周期最主要的目標在于避免懸垂引用,進而避免程序引用到非預期的數據。
(2)、借用檢查器:
Rust編譯器擁有一個借用檢查器 (borrow checker),它被用于比較不同的作用域并確定所有借用的合法性。
fn main() {let r; // ---------+-- 'a// |{ // |let x = 5; // -+-- 'b |r = &x; // | |} // -+ |// |println!("r: {}", r); // |
} // ---------+
在編譯過程中,Rust會比較兩段生命周期的大小,并發現r
擁有生命周期a
,但卻指向了擁有生命周期b
的內存。這段程序會由于b
比a
短而被拒絕通過編譯:被引用對象的存在范圍短于引用者。
(3)、生命周期標注語法:
生命周期的標注并不會改變任何引用的生命周期長度。如同使用了泛型參數的函數可以接收任何類型一樣,使用了泛型生命周期的函數也可以接收帶有任何生命周期的引用。在不影響生命周期的前提下,標注本身會被用于描述多個引用生命周期之間的關系。
生命周期的標注使用了一種明顯不同的語法:它們的參數名稱必須以撇號(')開頭,且通常使用全小寫字符。與泛型一樣,它們的名稱通常也會非常簡短。'a
被大部分開發者選擇作為默認使用的名稱。我們會將生命周期參數的標注填寫在&
引用運算符之后,并通過一個空格符來將標注與引用類型區分開來。
單個生命周期的標注本身并沒有太多意義,標注之所以存在是為了向Rust描述多個泛型生命周期參數之間的關系。
(4)、函數簽名中的生命周期標注:
如同泛型參數一樣,我們同樣需要在函數名與參數列表之間的尖括號內聲明泛型生命周期參數。在這個簽名中我們所表達的意思是:參數與返回值中的所有引用都必須擁有相同的生命周期。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}
這段代碼的函數簽名向Rust表明,函數所獲取的兩個字符串切片參數的存活時間,必須不短于給定的生命周期'a
。這個函數簽名同時也意味著,從這個函數返回的字符串切片也可以獲得不短于'a
的生命周期。
當我們在函數簽名中指定生命周期參數時,我們并沒有改變任何傳入值或返回值的生命周期。我們只是向借用檢查器指出了一些可以用于檢查非法調用的約束。
(5)、深入理解生命周期:
當函數返回一個引用時,返回類型的生命周期參數必須要與其中一個參數的生命周期參數相匹配。當返回的引用沒有 指向任何參數時,那么它只可能是指向了一個創建于函數內部的值,由于這個值會因為函數的結束而離開作用域,所以返回的內容也就變成了懸垂引用。
(6)、結構體定義中的生命周期標注:
struct ImportantExcerpt<'a> {part: &'a str,
}
fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().expect("Could not find a '.'");let i = ImportantExcerpt { part: first_sentence };
}
(7)、方法定義中的生命周期標注:
結構體字段中的生命周期名字總是需要被聲明在impl關鍵字之后,并被用于結構體名稱之后,因為這些生命周期是結構體類型的一部分。
在impl代碼塊的方法簽名中,引用可能是獨立的,也可能會與結構體字段中的引用的生命周期相關聯。另外,生命周期省略規則在大部分情況下都可以幫我們免去方法簽名中的生命周期標注。
我們定義一個名為level的方法,它僅有一個指向self的參數,并返回i32類型的值作為結果,這個結果并不會引用任何東西:
impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}
(8)、靜態生命周期:
Rust中還存在一種特殊的生命周期’static,它表示整個程序的執行期。所有的字符串字面量都擁有’static生命周期,示例:
let s: &'static str = "I have a static lifetime.";
字符串的文本被直接存儲在二進制程序中,并總是可用的。因此,所有字符串字面量的生命周期都是’static。