Rust Lang Book Ch.10 Generic Types, Traits. and Lifetimes
泛型
在函式中
fn largest<T>(list: &[T]) -> &T { let mut largest = list[0]; for item in list { if item > largest { largest = item; } } largest }
在struct中
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 };//注意如果是{x: 5, y: 4.0},那麼就不可能編譯,因為編譯器無法推測T到底是int還是float let float = Point { x: 1.0, y: 4.0 }; }
在enum中:
enum Result<T, E> { Ok(T), Err(E), }
在method中:
struct Point<T> { x: T, y: T, } impl<T> Point<T> {//注意要在impl之後就宣告一次T fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
能夠僅僅對泛型中的其中一種具現化提供實現
impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } }
struct和函式可以使用不同的泛型列表,例如struct本身使用T, U,方法可以使用W, V
impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } }
在Rust中,使用泛型的代價是幾乎沒有的。因為Rust會在編譯的時候對泛型做單態化(Monomorphization),為每個泛型所對應的具體實際型別生成對應的程式碼。例如,對應Some(5)和Some(5.0),編譯器會識別到i32和f64都是Option<T>對應的具體型別,因此會生成Option_i32和Option_f64兩個enum並完善對應邏輯。
Trait-特性
類似於其他語言的介面。一個trait內可以宣告多個函式簽名,這些函式在實現了之後就可以像正常成員函式一樣呼叫
pub trait Summary { fn summarize(&self) -> String;//注意這裡僅僅提供函式簽名。同時要注意這裡的成員變數也是要加&self的
//如果這裡提供了具體邏輯,就會成為預設實現。
然後再為每種type實現這些trait。與普通的實現不同,這裡要在impl之後寫對應trait的名字+for。注意,要為了某個型別實現trait具體邏輯,需要這個trait或者這個型別有一方是當前crate中的。例如,可以為Vec<T>實現Summary trait,但是不能為Vec<T>實現Display trait,這一限制被成為orphan rule,是內聚性的一種體現。
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內部直接提供函式具體邏輯,就會成為預設邏輯。為具體型別實現trait的時候就不需要為具體型別提供邏輯,可以直接放一個空的impl scope,例如impl Summary for NewsArticle {}
pub trait Summary { fn summarize(&self) -> String {//預設實現 String::from("(Read more...)") } }
trait的函式能夠呼叫trait的其他函式,因此,合理搭配預設邏輯和需要override的邏輯能夠起到事倍功半的效果。
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());
Trait作為函式引數
trait可以作為函式引數,但是需要搭配impl關鍵字或者Trait Bound Syntax一同使用。
impl關鍵字
pub fn notify(item: &impl Summary) {//只有實現了Summary的型別才能作為引數接受 println!("Breaking news! {}", item.summarize()); }
Trait Bound Syntax:搭配泛型使用
pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize()); }
用+可以要求引數同時滿足多個Traits
pub fn notify(item: &(impl Summary + Display)) {
pub fn notify<T: Summary + Display>(item: &T) {
Trait Bounds加上+加上where語句也可以完成這一任務,而且更加清晰:
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug {
Trait作為返回值
可以使用impl Trait語法來返回實現了具體trait的型別示例。但是,return impl Trait要求你的函式只可能返回一種型別,比如NewArticle和Tweet都實現了Summary trait,想要根據情況返回NewArticle或者Tweet就是不行的。
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, } }
如下所示,return impl TraitName只能返回單一的型別,否則就會報錯: expected Struct XXX, found struct YYY
fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( "Penguins win the Stanley Cup Championship!", ), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } else { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } | |_|_________^ expected struct `NewsArticle`, found struct `Tweet` } | |_____- `if` and `else` have incompatible types }
Trait作為其他Trait的條件之一
實現一個滿足了某個trait bounds對應的型別上面的新trait稱為blanket implementation
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
Lifetimes
引用的生命週期一般是通過推斷得到的,但是也可以有額外的註解來自定義生命週期。
Rust使用Borrow Checker來檢查變數的生命週期,並且要求引用的生命週期一定要被覆蓋在對應變數的生命週期之內,否則就報錯。
{ let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!("r: {}", r); // | } // ---------+
當遇到Borrow Checker無法確定生命週期的情況時,即引用進入一個函式再從一個函式返回時,編譯器會直接報錯。
fn longest(x: &str, y: &str) -> &str { ^ expected lifetime parameter //報錯的原因是Borrow Checker不知道返回的究竟是x還是y,因而無法追蹤引用的生命週期 if x.len() > y.len() { x } else { y } } fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); }
此時需要告訴編譯器引用的生命週期,加'a即為生命週期註解,註解本身沒有什麼意義,可以認為是在限制多個引用的生命週期的關係不會相互影響。
&i32 // a reference &'a i32 // a reference with an explicit lifetime &'a mut i32 // a mutable reference with an explicit lifetime
Borrow Checker將拒絕不滿足annotation的引用作為引數。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { //這裡表示引數x,y和返回值的生命週期是必須一致的
//在實際上,這表示返回的生命週期至少要和x,y中生命週期最短的一個一樣長
//x, y生命週期最短的那個必須覆蓋返回值的生命週期
if x.len() > y.len() { x } else { y } }
fn main() { let string1 = String::from("long string is long"); let result; { let string2 = String::from("xyz"); result = longest(string1.as_str(), string2.as_str()); ^^^^^^^ borrowed value does not live long enough } println!("The longest string is {}", result); }
如果返回值僅僅與其中一個引數有關,那就只需要宣告這個引數與返回值生命週期的關係。
fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
如果宣告和實際關係不符,編譯器會報錯:
1 | fn longest<'a>(x: &'a str, y: &str) -> &'a str { | ---- help: add explicit lifetime `'a` to the type of `y`: `&'a str` 2 | y | ^ lifetime `'a` required
Struct和Lifetime Annotation
對於成員變數是引用的情況,也可以新增lifetime annotation,但是需要注意所有的引用成員變數都要註明同樣的生命週期。
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, }; }
Lifetime Elision
有的時候無需用'a註明生命週期,這主要是Rust編譯器對某些應用場景會自動標上生命週期,這被成為lifetime elision rules。當然,Rust編譯器如果猜不出引用的生命週期就會報錯了,而這時候就需要程式設計師來標註。
函式或者成員函式的引數被稱為input lifetimes,返回值則被稱為output lifetimes。
首先,編譯器為每個引數分配一個lifetime parameters,比如'a, 'b, 'c,以此類推,例如fn foo<'a>(x: &'a i32)和fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。接著,如果只有一個input lifetime parameter,那麼所有返回值中的引用的生命週期都與這個引數相同。例如fn foo<'a>(x: &'a i32) -> &'a i32。最後,如果存在多個input lifetime parameters,但是其中一個是&self,那麼編譯器自動設定所有output lifetime parameters與&self或者&mut self的生命週期相同。
所以,對於fn first_word(s: &str) -> &str {這樣一個函式,編譯器能夠自動設定生命週期的關聯。
impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("Attention please: {}", announcement); self.part//因為一個input lifetime parameter是self,所以這裡設定返回值的生命週期和&self繫結。 } }
The Static Lifetime
這裡靜態生命週期指的是在整個程式執行期間,都必須要有效。
let s: &'static str = "I have a static lifetime.";