The Rust Programming Language 學習 (九)

泛型

每一個編程語言都有高效處理重復概念的工具。在 Rust 中其工具之一就是 泛型(generics)。泛型是具體類型或其他屬性的抽象替代。我們可以表達泛型的屬性,比如他們的行為或如何與其他泛型相關聯,而不需要在編寫和編譯代碼時知道他們在這里實際上代表什么

同理為了編寫一份可以用于多種具體值的代碼,函數并不知道其參數為何值,這時就可以讓函數獲取泛型而不是像 i32 或 String 這樣的具體類型。我們已經使用過的 Option<T> Vec<T>HashMap<K, V>Result<T, E> 這些泛型了。

我們可以使用泛型為函數簽名或結構體等項創建定義,這樣它們就可以用于多種不同的具體數據類型。讓我們看看如何使用泛型定義函數、結構體、枚舉和方法,然后我們將討論泛型如何影響代碼性能。

在函數定義中使用泛型

當使用泛型定義函數時,本來在函數簽名中指定參數和返回值的類型的地方,會改用泛型來表示。采用這種技術,使得代碼適應性更強,從而為函數的調用者提供更多的功能,同時也避免了代碼的重復。

fn largest_i32(list: &[i32]) -> i32 {let mut largest = list[0];for &item in list.iter() {if item > largest {largest = item;}}largest
}fn largest_char(list: &[char]) -> char {let mut largest = list[0];for &item in list.iter() {if item > largest {largest = item;}}largest
}fn main() {let number_list = vec![34, 50, 25, 100, 65];let result = largest_i32(&number_list);println!("The largest number is {}", result);assert_eq!(result, 100);let char_list = vec!['y', 'm', 'a', 'q'];let result = largest_char(&char_list);println!("The largest char is {}", result);assert_eq!(result, 'y');
}

為了參數化新函數中的這些類型,我們也需要為類型參數取個名字,道理和給函數的形參起名一樣。任何標識符都可以作為類型參數的名字。這里選用 T,因為傳統上來說,Rust 的參數名字都比較短,通常就只有一個字母,同時,Rust 類型名的命名規范是駱駝命名法(CamelCase)。T 作為 “type” 的縮寫是大部分 Rust 開發者的首選

如果要在函數體中使用參數,就必須在函數簽名中聲明它的名字,好讓編譯器知道這個名字指代的是什么。同理,當在函數簽名中使用一個類型參數時,必須在使用它之前就聲明它。為了定義泛型版本的 largest 函數,類型參數聲明位于函數名稱與參數列表中間的尖括號 <> 中,像這樣:

fn largest<T>(list: &[T]) -> T {

可以這樣理解這個定義:函數 largest 有泛型類型 T。它有個參數 list,其類型是元素為 T 的 slice。largest 函數的返回值類型也是 T。

結構體中定義泛型

同樣也可以用 <> 語法來定義結構體,它包含一個或多個泛型參數類型字段。

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 };
}

其語法類似于函數定義中使用泛型。首先,必須在結構體名稱后面的尖括號中聲明泛型參數的名稱。接著在結構體定義中可以指定具體數據類型的位置使用泛型類型。

字段 x 和 y 的類型必須相同,因為他們都有相同的泛型類型 T

如果想要定義一個 x 和 y 可以有不同類型且仍然是泛型的 Point 結構體,我們可以使用多個泛型類型參數。

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 };
}

現在所有的 Point 實例都合法了!你可以在定義中使用任意多的泛型類型參數,不過太多的話,代碼將難以閱讀和理解。當你的代碼中需要許多泛型類型時,它可能表明你的代碼需要重構,分解成更小的結構。

枚舉中定義的泛型

和結構體類似,枚舉也可以在成員中存放泛型數據類型。比如我們曾用過標準庫提供的 Option 枚舉,這里再回顧一下:

enum Option<T> {Some(T),None,
}

現在這個定義應該更容易理解了。如你所見 Option<T> 是一個擁有泛型 T 的枚舉,它有兩個成員:Some,它存放了一個類型 T 的值,和不存在任何值的 None。通過 Option<T> 枚舉可以表達有一個可能的值的抽象概念,同時因為 Option 是泛型的,無論這個可能的值是什么類型都可以使用這個抽象。

枚舉也可以擁有多個泛型類型。

enum Result<T, E> {Ok(T),Err(E),
}

Result 枚舉有兩個泛型類型,T 和 E。Result 有兩個成員:Ok,它存放一個類型 T 的值,而 Err 則存放一個類型 E 的值。這個定義使得 Result 枚舉能很方便的表達任何可能成功(返回 T 類型的值)也可能失敗(返回 E 類型的值)的操作。實際上,這就是我們在示例 用來打開文件的方式:當成功打開文件的時候,T 對應的是 std::fs::File 類型;而當打開文件出現問題時,E 的值則是 std::io::Error 類型。

當你意識到代碼中定義了多個結構體或枚舉,它們不一樣的地方只是其中的值的類型的時候,不妨通過泛型類型來避免重復。

方法定義中的泛型

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<T> 上定義了一個叫做 x 的方法來返回字段 x 中數據的引用:

注意必須在 impl 后面聲明 T,這樣就可以在 Point<T> 上實現的方法中使用它了。在 impl 之后聲明泛型 T ,這樣 Rust 就知道 Point 的尖括號中的類型是泛型而不是具體類型。

例如,可以選擇為 Point<f32> 實例實現方法,而不是為泛型 Point 實例。示例展示了一個沒有在 impl 之后(的尖括號)聲明泛型的例子,這里使用了一個具體類型,f32:


#![allow(unused)]
fn main() {
struct Point<T> {x: T,y: T,
}impl Point<f32> {fn distance_from_origin(&self) -> f32 {(self.x.powi(2) + self.y.powi(2)).sqrt()}
}
}

這段代碼意味著 Point<f32> 類型會有一個方法 distance_from_origin,而其他 T 不是 f32 類型的 Point<T> 實例則沒有定義此方法。這個方法計算點實例與坐標 (0.0, 0.0) 之間的距離,并使用了只能用于浮點型的數學運算符。

struct Point<T, U> {x: T,y: U,
}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,}}
}fn main() {let p1 = Point { x: 5, y: 10.4 };let p2 = Point { x: "Hello", y: 'c'};let p3 = p1.mixup(p2);println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

在 main 函數中,定義了一個有 i32 類型的 x(其值為 5)和 f64 的 y(其值為 10.4)的 Point。p2 則是一個有著字符串 slice 類型的 x(其值為 “Hello”)和 char 類型的 y(其值為 c)的 Point。在 p1 上以 p2 作為參數調用 mixup 會返回一個 p3,它會有一個 i32 類型的 x,因為 x 來自 p1,并擁有一個 char 類型的 y,因為 y 來自 p2。println! 會打印出 p3.x = 5, p3.y = c

這個例子的目的是展示一些泛型通過 impl 聲明而另一些通過方法定義聲明的情況。這里泛型參數 T 和 U 聲明于 impl 之后,因為他們與結構體定義相對應。而泛型參數 V 和 W 聲明于 fn mixup 之后,因為他們只是相對于方法本身的。

泛型代碼的性能

在閱讀本部分內容的同時,你可能會好奇使用泛型類型參數是否會有運行時消耗。好消息是:Rust 實現了泛型,使得使用泛型類型參數的代碼相比使用具體類型并沒有任何速度上的損失。

Rust 通過在編譯時進行泛型代碼的 單態化(monomorphization)來保證效率。單態化是一個通過填充編譯時使用的具體類型,將通用代碼轉換為特定代碼的過程。

編譯器尋找所有泛型代碼被調用的位置并使用泛型代碼針對具體類型生成代碼。

讓我們看看一個使用標準庫中 Option 枚舉的例子:

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

當 Rust 編譯這些代碼的時候,它會進行單態化。編譯器會讀取傳遞給 Option<T> 的值并發現有兩種 Option<T>:一個對應 i32 另一個對應 f64。為此,它會將泛型定義 Option<T> 展開為 Option_i32Option_f64,接著將泛型定義替換為這兩個具體的定義。

編譯器生成的單態化版本的代碼看起來像這樣,并包含將泛型 Option<T> 替換為編譯器創建的具體定義后的用例代碼:

enum Option_i32 {Some(i32),None,
}enum Option_f64 {Some(f64),None,
}fn main() {let integer = Option_i32::Some(5);let float = Option_f64::Some(5.0);
}

我們可以使用泛型來編寫不重復的代碼,而 Rust 將會為每一個實例編譯其特定類型的代碼。這意味著在使用泛型時沒有運行時開銷;當代碼運行,它的執行效率就跟好像手寫每個具體定義的重復代碼一樣。這個單態化過程正是 Rust 泛型在運行時極其高效的原因。

trait:定義共享的行為

trait 告訴 Rust 編譯器某個特定類型擁有可能與其他類型共享的功能。可以通過 trait 以一種抽象的方式定義共享的行為。可以使用 trait bounds 指定泛型是任何擁有特定行為的類型。

注意:trait 類似于其他語言中常被稱為 接口(interfaces)的功能,雖然有一些不同。

定義 trait

一個類型的行為由其可供調用的方法構成。如果可以對不同類型調用相同的方法的話,這些類型就可以共享相同的行為了。trait 定義是一種將方法簽名組合起來的方法,目的是定義一個實現某些目的所必需的行為的集合。

例如,這里有多個存放了不同類型和屬性文本的結構體:結構體 NewsArticle 用于存放發生于世界各地的新聞故事,而結構體 Tweet 最多只能存放 280 個字符的內容,以及像是否轉推或是否是對推友的回復這樣的元數據。

我們想要創建一個多媒體聚合庫用來顯示可能儲存在 NewsArticle 或 Tweet 實例中的數據的總結。每一個結構體都需要的行為是他們是能夠被總結的,這樣的話就可以調用實例的 summarize 方法來請求總結。示例中展示了一個表現這個概念的 Summary trait 的定義:

pub trait Summary {fn summarize(&self) -> String;
}

這里使用 trait 關鍵字來聲明一個 trait,后面是 trait 的名字,在這個例子中是 Summary。在大括號中聲明描述實現這個 trait 的類型所需要的行為的方法簽名,在這個例子中是 fn summarize(&self) -> String

在方法簽名后跟分號,而不是在大括號中提供其實現。接著每一個實現這個 trait 的類型都需要提供其自定義行為的方法體,編譯器也會確保任何實現 Summary trait 的類型都擁有與這個簽名的定義完全一致的 summarize 方法。

trait 體中可以有多個方法:一行一個方法簽名且都以分號結尾。

為類型實現trait

現在我們定義了 Summary trait,接著就可以在多媒體聚合庫中需要擁有這個行為的類型上實現它了。示例 中展示了 NewsArticle 結構體上 Summary trait 的一個實現,它使用標題、作者和創建的位置作為 summarize 的返回值。對于 Tweet 結構體,我們選擇將 summarize 定義為用戶名后跟推文的全部文本作為返回值,并假設推文內容已經被限制為 280 字符以內。

#![allow(unused)]
fn main() {
pub trait Summary {fn summarize(&self) -> String;
}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 關鍵字之后,我們提供需要實現 trait 的名稱,接著是 for 和需要實現 trait 的類型的名稱。在 impl 塊中,使用 trait 定義中的方法簽名,不過不再后跟分號,而是需要在大括號中編寫函數體來為特定類型實現 trait 方法所擁有的行為。

一旦實現了 trait,我們就可以用與 NewsArticle 和 Tweet 實例的非 trait 方法一樣的方式調用 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());

這會打印出 1 new tweet: horse_ebooks: of course, as you probably already know, people

注意因為示例 中我們在相同的 lib.rs 里定義了 Summary trait 和 NewsArticle 與 Tweet 類型,所以他們是位于同一作用域的。如果這個 lib.rs 是對應 aggregator crate 的,而別人想要利用我們 crate 的功能為其自己的庫作用域中的結構體實現 Summary trait。首先他們需要將 trait 引入作用域。這可以通過指定 use aggregator::Summary; 實現,這樣就可以為其類型實現 Summary trait 了。Summary 還必須是公有 trait 使得其他 crate 可以實現它,這也是為什么示例 中將 pub 置于 trait 之前。

實現 trait 時需要注意的一個限制是,只有當 trait 或者要實現 trait 的類型位于 crate 的本地作用域時,才能為該類型實現 trait。例如,可以為 aggregator crate 的自定義類型 Tweet 實現如標準庫中的 Display trait,這是因為 Tweet 類型位于 aggregator crate 本地的作用域中。類似地,也可以在 aggregator crate 中為 Vec<T> 實現 Summary,這是因為 Summary trait 位于 aggregator crate 本地作用域中。

實現 trait 時需要注意的一個限制是,只有當 trait 或者要實現 trait 的類型位于 crate 的本地作用域時,才能為該類型實現 trait。例如,可以為 aggregator crate 的自定義類型 Tweet 實現如標準庫中的 Display trait,這是因為 Tweet 類型位于 aggregator crate 本地的作用域中。類似地,也可以在 aggregator crate 中為Vec<T>實現 Summary,這是因為 Summary trait 位于 aggregator crate 本地作用域中。

默認實現

有時為 trait 中的某些或全部方法提供默認的行為,而不是在每個類型的每個實現中都定義自己的行為是很有用的。這樣當為某個特定類型實現 trait 時,可以選擇保留或重載每個方法的默認行為。

示例 中展示了如何為 Summary trait 的 summarize 方法指定一個默認的字符串值,而不是像那樣只是定義方法簽名

fn main() {
pub trait Summary {fn summarize(&self) -> String {String::from("(Read more...)")}
}

如果想要對 NewsArticle 實例使用這個默認實現,而不是定義一個自己的實現,則可以通過impl Summary for NewsArticle {}指定一個空的 impl 塊。

雖然我們不再直接為 NewsArticle 定義 summarize 方法了,但是我們提供了一個默認實現并且指定 NewsArticle 實現 Summary trait。因此,我們仍然可以對 NewsArticle 實例調用 summarize 方法,如下所示:

let article = 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 besthockey team in the NHL."),
};println!("New article available! {}", article.summarize());

這段代碼會打印 New article available! (Read more...)

為 summarize 創建默認實現并不要求對示例 Tweet 上的 Summary 實現做任何改變。其原因是重載一個默認實現的語法與實現沒有默認實現的 trait 方法的語法一樣。

默認實現允許調用相同 trait 中的其他方法,哪怕這些方法沒有默認實現。如此,trait 可以提供很多有用的功能而只需要實現指定一小部分內容。例如,我們可以定義 Summary trait,使其具有一個需要實現的 summarize_author 方法,然后定義一個 summarize 方法,此方法的默認實現調用 summarize_author 方法:

pub trait Summary {fn summarize_author(&self) -> String;fn summarize(&self) -> String {format!("(Read more from {}...)", self.summarize_author())}
}

為了使用這個版本的 Summary,只需在實現 trait 時定義 summarize_author 即可:

impl Summary for Tweet {fn summarize_author(&self) -> String {format!("@{}", self.username)}
}

一旦定義了 summarize_author,我們就可以對 Tweet 結構體的實例調用 summarize 了,而 summarize 的默認實現會調用我們提供的 summarize_author 定義。因為實現了 summarize_author,Summary trait 就提供了 summarize 方法的功能,且無需編寫更多的代碼。

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 more from @horse_ebooks...)

請注意,無法從相同方法的重載實現中調用默認方法。

trait 作為參數

知道了如何定義 trait 和在類型上實現這些 trait 之后,我們可以探索一下如何使用 trait 來接受多種不同類型的參數。

例如在示例 中為 NewsArticle 和 Tweet 類型實現了 Summary trait。我們可以定義一個函數 notify 來調用其參數 item 上的 summarize 方法,該參數是實現了 Summary trait 的某種類型。為此可以使用 impl Trait 語法,像這樣:

pub fn notify(item: impl Summary) {println!("Breaking news! {}", item.summarize());
}

對于 item 參數,我們指定了 impl 關鍵字和 trait 名稱,而不是具體的類型。該參數支持任何實現了指定 trait 的類型。在 notify 函數體中,可以調用任何來自 Summary trait 的方法,比如 summarize。我們可以傳遞任何 NewsArticle 或 Tweet 的實例來調用 notify。任何用其它如 String 或 i32 的類型調用該函數的代碼都不能編譯,因為它們沒有實現 Summary。

Trait Bound 語法

impl Trait 語法適用于直觀的例子,它實際上是一種較長形式語法的語法糖。我們稱為 trait bound,它看起來像:

pub fn notify<T: Summary>(item: T) {println!("Breaking news! {}", item.summarize());
}

這與之前的例子相同,不過稍微冗長了一些。trait bound 與泛型參數聲明在一起,位于尖括號中的冒號后面。

impl Trait 很方便,適用于短小的例子。trait bound 則適用于更復雜的場景。例如,可以獲取兩個實現了 Summary 的參數。使用 impl Trait 的語法看起來像這樣:

pub fn notify(item1: impl Summary, item2: impl Summary) {

這適用于 item1 和 item2 允許是不同類型的情況(只要它們都實現了 Summary)。不過如果你希望強制它們都是相同類型呢?這只有在使用 trait bound 時才有可能:

pub fn notify<T: Summary>(item1: T, item2: T) {

泛型 T 被指定為 item1 和 item2 的參數限制,如此傳遞給參數 item1 和 item2 值的具體類型必須一致。

通過 + 指定多個 trait bound

如果 notify 需要顯示 item 的格式化形式,同時也要使用 summarize 方法,那么 item 就需要同時實現兩個不同的 trait:Display 和 Summary。這可以通過 + 語法實現:

pub fn notify(item: impl Summary + Display) {

+ 語法也適用于泛型的 trait bound:

pub fn notify<T: Summary + Display>(item: T) {

通過指定這兩個 trait bound,notify 的函數體可以調用 summarize 并使用 {} 來格式化 item。

通過 where 簡化 trait bound

然而,使用過多的 trait bound 也有缺點。每個泛型有其自己的 trait bound,所以有多個泛型參數的函數在名稱和參數列表之間會有很長的 trait bound 信息,這使得函數簽名難以閱讀。為此,Rust 有另一個在函數簽名之后的 where 從句中指定 trait bound 的語法。所以除了這么寫:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

還可以像這樣使用 where 從句:

fn some_function<T, U>(t: T, u: U) -> i32where T: Display + Clone,U: Clone + Debug
{

這個函數簽名就顯得不那么雜亂,函數名、參數列表和返回值類型都離得很近,看起來跟沒有那么多 trait bounds 的函數很像。

返回實現了 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,}
}

通過使用 impl Summary 作為返回值類型,我們指定了 returns_summarizable 函數返回某個實現了 Summary trait 的類型,但是不確定其具體的類型。在這個例子中 returns_summarizable 返回了一個 Tweet,不過調用方并不知情。

返回一個只是指定了需要實現的 trait 的類型的能力在閉包和迭代器場景十分的有用.閉包和迭代器創建只有編譯器知道的類型,或者是非常非常長的類型。impl Trait 允許你簡單的指定函數返回一個Iterator 而無需寫出實際的冗長的類型。

不過這只適用于返回單一類型的情況。例如,這段代碼的返回值類型指定為返回 impl Summary,但是返回了 NewsArticle 或 Tweet 就行不通:

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 besthockey 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,}}
}

這里嘗試返回 NewsArticle 或 Tweet。這不能編譯,因為 impl Trait 工作方式的限制。

使用 trait bounds 來修復 largest 函數

先來看一下原來的largest函數

fn largest<T>(list: &[T]) -> T {let mut largest = list[0];for &item in list.iter() {if item > largest {largest = item;}}largest
}fn main() {let number_list = vec![34, 50, 25, 100, 65];let result = largest(&number_list);println!("The largest number is {}", result);let char_list = vec!['y', 'm', 'a', 'q'];let result = largest(&char_list);println!("The largest char is {}", result);
}

現在你知道了如何使用泛型參數 trait bound 來指定所需的行為。

在 largest 函數體中我們想要使用大于運算符(>)比較兩個 T 類型的值。這個運算符被定義為標準庫中 trait std::cmp::PartialOrd 的一個默認方法。所以需要在 T 的 trait bound 中指定 PartialOrd,這樣 largest 函數可以用于任何可以比較大小的類型的 slice。因為 PartialOrd 位于 prelude 中所以并不需要手動將其引入作用域。將 largest 的簽名修改為如下

fn largest<T: PartialOrd>(list: &[T]) -> T {

但是如果編譯代碼的話,會出現一些不同的錯誤:

error[E0508]: cannot move out of type `[T]`, a non-copy slice--> src/main.rs:2:23|
2 |     let mut largest = list[0];|                       ^^^^^^^|                       ||                       cannot move out of here|                       help: consider using a reference instead: `&list[0]`error[E0507]: cannot move out of borrowed content--> src/main.rs:4:9|
4 |     for &item in list.iter() {|         ^----|         |||         |hint: to prevent move, use `ref item` or `ref mut item`|         cannot move out of borrowed content

錯誤的核心是cannot move out of type [T], a non-copy slice,對于非泛型版本的 largest 函數,我們只嘗試了尋找最大的 i32 和 char。正如第 4 章 “只在棧上的數據:拷貝” 部分討論過的,像 i32 和 char 這樣的類型是已知大小的并可以儲存在棧上,所以他們實現了 Copy trait。當我們將 largest 函數改成使用泛型后,現在 list 參數的類型就有可能是沒有實現 Copy trait 的。這意味著我們可能不能將 list[0] 的值移動到 largest 變量中,這導致了上面的錯誤。

為了只對實現了 Copy 的類型調用這些代碼,可以在 T 的 trait bounds 中增加 Copy!示例 中展示了一個可以編譯的泛型版本的 largest 函數的完整代碼,只要傳遞給 largest 的 slice 值的類型實現了 PartialOrd 和 Copy 這兩個 trait,例如 i32 和 char:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {let mut largest = list[0];for &item in list.iter() {if item > largest {largest = item;}}largest
}fn main() {let number_list = vec![34, 50, 25, 100, 65];let result = largest(&number_list);println!("The largest number is {}", result);let char_list = vec!['y', 'm', 'a', 'q'];let result = largest(&char_list);println!("The largest char is {}", result);
}

如果并不希望限制 largest 函數只能用于實現了 Copy trait 的類型,我們可以在 T 的 trait bounds 中指定 Clone 而不是 Copy。并克隆 slice 的每一個值使得 largest 函數擁有其所有權。使用 clone 函數意味著對于類似 String 這樣擁有堆上數據的類型,會潛在的分配更多堆上空間,而堆分配在涉及大量數據時可能會相當緩慢。

另一種 largest 的實現方式是返回在 slice 中 T 值的引用。如果我們將函數返回值從 T 改為 &T 并改變函數體使其能夠返回一個引用,我們將不需要任何 Clone 或 Copy 的 trait bounds 而且也不會有任何的堆分配。嘗試自己實現這種替代解決方式吧!

使用 trait bound 有條件地實現方法

通過使用帶有 trait bound 的泛型參數的 impl 塊,可以有條件地只為那些實現了特定 trait 的類型實現方法。


#![allow(unused)]
fn main() {
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);}}
}
}

也可以對任何實現了特定 trait 的類型有條件地實現 trait。對任何滿足特定 trait bound 的類型實現 trait 被稱為 blanket implementations,他們被廣泛的用于 Rust 標準庫中。

例如,標準庫為任何實現了 Display trait 的類型實現了 ToString trait。這個 impl 塊看起來像這樣:

impl<T: Display> ToString for T {// --snip--
}

因為標準庫有了這些 blanket implementation,我們可以對任何實現了 Display trait 的類型調用由 ToString 定義的 to_string 方法。例如,可以將整型轉換為對應的 String 值,因為整型實現了 Display:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

trait 和 trait bound 讓我們使用泛型類型參數來減少重復,并仍然能夠向編譯器明確指定泛型類型需要擁有哪些行為。因為我們向編譯器提供了 trait bound 信息,它就可以檢查代碼中所用到的具體類型是否提供了正確的行為。在動態類型語言中,如果我們嘗試調用一個類型并沒有實現的方法,會在運行時出現錯誤。Rust 將這些錯誤移動到了編譯時,甚至在代碼能夠運行之前就強迫我們修復錯誤。另外,我們也無需編寫運行時檢查行為的代碼,因為在編譯時就已經檢查過了,這樣相比其他那些不愿放棄泛型靈活性的語言有更好的性能。

這里還有一種泛型,我們一直在使用它甚至都沒有察覺它的存在,這就是 生命周期(lifetimes)。不同于其他泛型幫助我們確保類型擁有期望的行為,生命周期則有助于確保引用在我們需要他們的時候一直有效。

生命周期與引用有效性

Rust 中的每一個引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分時候生命周期是隱含并可以推斷的,正如大部分時候類型也是可以推斷的一樣。類似于當因為有多種可能類型的時候不得不注明類型,也會出現引用的生命周期以一些不同方式相關聯的情況,所以 Rust 需要我們使用泛型生命周期參數來注明他們的關系,這樣就能確保運行時實際使用的引用絕對是有效的。

生命周期的概念從某種程度上說不同于其他語言中類似的工具,毫無疑問這是 Rust 最與眾不同的功能。雖然本章不可能涉及到它全部的內容,我們會講到一些通常你可能會遇到的生命周期語法以便你熟悉這個概念。

生命周期避免了懸垂引用

生命周期的主要目標是避免懸垂引用,它會導致程序引用了非預期引用的數據。

{let r;{let x = 5;r = &x;}println!("r: {}", r);
}

外部作用域聲明了一個沒有初值的變量 r,而內部作用域聲明了一個初值為 5 的變量 x。在內部作用域中,我們嘗試將 r 的值設置為一個 x 的引用。接著在內部作用域結束后,嘗試打印出 r 的值。這段代碼不能編譯因為 r 引用的值在嘗試使用之前就離開了作用域。如下是錯誤信息:

error[E0597]: `x` does not live long enough--> src/main.rs:7:5|
6  |         r = &x;|              - borrow occurs here
7  |     }|     ^ `x` dropped here while still borrowed
...
10 | }| - borrowed value needs to live until here

變量 x 并沒有 “存在的足夠久”。其原因是 x 在到達第 7 行內部作用域結束時就離開了作用域。不過 r 在外部作用域仍是有效的;作用域越大我們就說它 “存在的越久”。如果 Rust 允許這段代碼工作,r 將會引用在 x 離開作用域時被釋放的內存,這時嘗試對 r 做任何操作都不能正常工作。那么 Rust 是如何決定這段代碼是不被允許的呢?這得益于借用檢查器。

借用檢查器

Rust 編譯器有一個 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。

{let r;                // ---------+-- 'a//          |{                     //          |let x = 5;        // -+-- 'b  |r = &x;           //  |       |}                     // -+       |//          |println!("r: {}", r); //          |
}                         // ---------+

這里將 r 的生命周期標記為 'a 并將 x 的生命周期標記為 'b。如你所見,內部的 'b 塊要比外部的生命周期 'a 小得多。在編譯時,Rust 比較這兩個生命周期的大小,并發現 r 擁有生命周期 'a,不過它引用了一個擁有生命周期 'b 的對象。程序被拒絕編譯,因為生命周期 'b 比生命周期 'a 要小:被引用的對象比它的引用者存在的時間更短。

函數中的泛型生命周期

讓我們來編寫一個函數,返回兩個字符串 slice 中較長的那一個。這個函數獲取兩個字符串 slice 并返回一個字符串 slice。一旦我們實現了 longest 函數,示例 中的代碼應該會打印出 The longest string is abcd

fn main() {let string1 = String::from("abcd");let string2 = "xyz";let result = longest(string1.as_str(), string2);println!("The longest string is {}", result);
}

請注意,這個函數獲取作為引用的字符串 slice,因為我們不希望 longest 函數獲取參數的所有權.

fn longest(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}

相應地會出現如下有關生命周期的錯誤

error[E0106]: missing lifetime specifier--> src/main.rs:1:33|
1 | fn longest(x: &str, y: &str) -> &str {|                                 ^ expected lifetime parameter|= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`

提示文本揭示了返回值需要一個泛型生命周期參數,因為 Rust 并不知道將要返回的引用是指向 x 或 y。事實上我們也不知道,因為函數體中 if 塊返回一個 x 的引用而 else 塊返回一個 y 的引用!

當我們定義這個函數的時候,并不知道傳遞給函數的具體值,所以也不知道到底是 if 還是 else 會被執行。我們也不知道傳入的引用的具體生命周期,所以也就不能像示例那樣通過觀察作用域來確定返回的引用是否總是有效。借用檢查器自身同樣也無法確定,因為它不知道 x 和 y 的生命周期是如何與返回值的生命周期相關聯的。為了修復這個錯誤,我們將增加泛型生命周期參數來定義引用間的關系以便借用檢查器可以進行分析。

生命周期標注語句

生命周期標注并不改變任何引用的生命周期的長短。與當函數簽名中指定了泛型類型參數后就可以接受任何類型一樣,當指定了泛型生命周期后函數也能接受任何生命周期的引用。生命周期標注描述了多個引用生命周期相互的關系,而不影響其生命周期。

生命周期標注有著一個不太常見的語法:生命周期參數名稱必須以撇號(')開頭,其名稱通常全是小寫,類似于泛型其名稱非常短。'a 是大多數人默認使用的名稱。生命周期參數標注位于引用的 & 之后,并有一個空格來將引用類型與生命周期標注分隔開。

這里有一些例子:我們有一個沒有生命周期參數的 i32 的引用,一個有叫做 'a 的生命周期參數的 i32 的引用,和一個生命周期也是 'a 的 i32 的可變引用:

&i32        // 引用
&'a i32     // 帶有顯式生命周期的引用
&'a mut i32 // 帶有顯式生命周期的可變引用

單個生命周期標注本身沒有多少意義,因為生命周期標注告訴 Rust 多個引用的泛型生命周期參數如何相互聯系的。例如如果函數有一個生命周期'a的 i32 的引用的參數 first。還有另一個同樣是生命周期 'a 的 i32 的引用的參數 second。這兩個生命周期標注意味著引用 first 和 second 必須與這泛型生命周期存在得一樣久。

函數簽名中的生命周期標注

現在來看看 longest 函數的上下文中的生命周期。就像泛型類型參數,泛型生命周期參數需要聲明在函數名和參數列表間的尖括號中。這里我們想要告訴 Rust 關于參數中的引用和返回值之間的限制是他們都必須擁有相同的生命周期,就像示例 中在每個引用中都加上了 'a 那樣:


#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}
}

這段代碼能夠編譯并會產生我們希望得到的示例 中的 main 函數的結果。

現在函數簽名表明對于某些生命周期 'a,函數會獲取兩個參數,他們都是與生命周期'a存在的一樣長的字符串 slice。函數會返回一個同樣也與生命周期 'a 存在的一樣長的字符串 slice。它的實際含義是 longest 函數返回的引用的生命周期與傳入該函數的引用的生命周期的較小者一致。這就是我們告訴 Rust 需要其保證的約束條件。記住通過在函數簽名中指定生命周期參數時,我們并沒有改變任何傳入值或返回值的生命周期,而是指出任何不滿足這個約束條件的值都將被借用檢查器拒絕。注意 longest 函數并不需要知道 x 和 y 具體會存在多久,而只需要知道有某個可以被 'a 替代的作用域將會滿足這個簽名。

當在函數中使用生命周期標注時,這些標注出現在函數簽名中,而不存在于函數體中的任何代碼中。這是因為 Rust 能夠分析函數中代碼而不需要任何協助,不過當函數引用或被函數之外的代碼引用時,讓 Rust 自身分析出參數或返回值的生命周期幾乎是不可能的。這些生命周期在每次函數被調用時都可能不同。這也就是為什么我們需要手動標記生命周期。

當具體的引用被傳遞給 longest 時,被 'a 所替代的具體生命周期是 x 的作用域與 y 的作用域相重疊的那一部分。換一種說法就是泛型生命周期 'a 的具體生命周期等同于 x 和 y 的生命周期中較小的那一個。因為我們用相同的生命周期參數'a標注了返回的引用值,所以返回的引用值就能保證在 x 和 y 中較短的那個生命周期結束之前保持有效。

讓我們看看如何通過傳遞擁有不同具體生命周期的引用來限制 longest 函數的使用。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}fn main() {let string1 = String::from("long string is long");{let string2 = String::from("xyz");let result = longest(string1.as_str(), string2.as_str());println!("The longest string is {}", result);}
}

在這個例子中,string1 直到外部作用域結束都是有效的,string2 則在內部作用域中是有效的,而 result 則引用了一些直到內部作用域結束都是有效的值。借用檢查器認可這些代碼;它能夠編譯和運行,并打印出 The longest string is long string is long。

接下來,讓我們嘗試另外一個例子,該例子揭示了 result 的引用的生命周期必須是兩個參數中較短的那個。以下代碼將 result 變量的聲明移動出內部作用域,但是將 result 和 string2 變量的賦值語句一同留在內部作用域中。接著,使用了變量 result 的 println! 也被移動到內部作用域之外。注意示例中的代碼不能通過編譯:

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());}println!("The longest string is {}", result);
}

如果嘗試編譯會出現如下錯誤:

error[E0597]: `string2` does not live long enough--> src/main.rs:6:44|
6 |         result = longest(string1.as_str(), string2.as_str());|                                            ^^^^^^^ borrowed value does not live long enough
7 |     }|     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);|                                          ------ borrow later used here

錯誤表明為了保證 println! 中的 result 是有效的,string2 需要直到外部作用域結束都是有效的。Rust 知道這些是因為(longest)函數的參數和返回值都使用了相同的生命周期參數 'a

如果從人的角度讀上述代碼,我們可能會覺得這個代碼是正確的。 string1 更長,因此 result 會包含指向 string1 的引用。因為 string1 尚未離開作用域,對于 println! 來說 string1 的引用仍然是有效的。然而,我們通過生命周期參數告訴 Rust 的是: longest 函數返回的引用的生命周期應該與傳入參數的生命周期中較短那個保持一致。因此,借用檢查器不允許示例 中的代碼,因為它可能會存在無效的引用。

深入理解生命周期

指定生命周期參數的正確方式依賴函數實現的具體功能。例如,如果將 longest 函數的實現修改為總是返回第一個參數而不是最長的字符串 slice,就不需要為參數 y 指定一個生命周期。如下代碼將能夠編譯:

#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {x
}
}

在這個例子中,我們為參數 x 和返回值指定了生命周期參數 'a,不過沒有為參數 y 指定,因為 y 的生命周期與參數 x 和返回值的生命周期沒有任何關系。

當從函數返回一個引用,返回值的生命周期參數需要與一個參數的生命周期參數相匹配。如果返回的引用 沒有 指向任何一個參數,那么唯一的可能就是它指向一個函數內部創建的值,它將會是一個懸垂引用,因為它將會在函數結束時離開作用域。嘗試考慮這個并不能編譯的 longest 函數實現:

fn longest<'a>(x: &str, y: &str) -> &'a str {let result = String::from("really long string");result.as_str()
}

即便我們為返回值指定了生命周期參數 'a,這個實現卻編譯失敗了,因為返回值的生命周期與參數完全沒有關聯。這里是會出現的錯誤信息:

error[E0597]: `result` does not live long enough--> src/main.rs:3:5|
3 |     result.as_str()|     ^^^^^^ does not live long enough
4 | }| - borrowed value only lives until here|
note: borrowed value must be valid for the lifetime 'a as defined on the
function body at 1:1...--> src/main.rs:1:1|
1 | / fn longest<'a>(x: &str, y: &str) -> &'a str {
2 | |     let result = String::from("really long string");
3 | |     result.as_str()
4 | | }| |_^

出現的問題是 result 在 longest 函數的結尾將離開作用域并被清理,而我們嘗試從函數返回一個 result 的引用。無法指定生命周期參數來改變懸垂引用,而且 Rust 也不允許我們創建一個懸垂引用。在這種情況,最好的解決方案是返回一個有所有權的數據類型而不是一個引用,這樣函數調用者就需要負責清理這個值了。

綜上,生命周期語法是用于將函數的多個參數與其返回值的生命周期進行關聯的。一旦他們形成了某種關聯,Rust 就有了足夠的信息來允許內存安全的操作并阻止會產生懸垂指針亦或是違反內存安全的行為。

結構體定義中的生命周期標注

目前為止,我們只定義過有所有權類型的結構體。接下來,我們將定義包含引用的結構體,不過這需要為結構體定義中的每一個引用添加生命周期標注。示例 中有一個存放了一個字符串 slice 的結構體 ImportantExcerpt:

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 };
}

這個結構體有一個字段,part,它存放了一個字符串 slice,這是一個引用。類似于泛型參數類型,必須在結構體名稱后面的尖括號中聲明泛型生命周期參數,以便在結構體定義中使用生命周期參數。這個標注意味著 ImportantExcerpt 的實例不能比其 part 字段中的引用存在的更久。

這里的 main 函數創建了一個 ImportantExcerpt 的實例,它存放了變量 novel 所擁有的 String 的第一個句子的引用。novel 的數據在 ImportantExcerpt 實例創建之前就存在。另外,直到 ImportantExcerpt 離開作用域之后 novel 都不會離開作用域,所以 ImportantExcerpt 實例中的引用是有效的。

生命周期省略(Lifetime Elision)

現在我們已經知道了每一個引用都有一個生命周期,而且我們需要為那些使用了引用的函數或結構體指定生命周期。然而如示例 所示,它沒有生命周期標注卻能編譯成功:


#![allow(unused)]
fn main() {
fn first_word(s: &str) -> &str {let bytes = s.as_bytes();for (i, &item) in bytes.iter().enumerate() {if item == b' ' {return &s[0..i];}}&s[..]
}
}

這個函數沒有生命周期標注卻能編譯是由于一些歷史原因:在早期版本(pre-1.0)的 Rust 中,這的確是不能編譯的。每一個引用都必須有明確的生命周期。那時的函數簽名將會寫成這樣:

fn first_word<'a>(s: &'a str) -> &'a str {

在編寫了很多 Rust 代碼后,Rust 團隊發現在特定情況下 Rust 開發者們總是重復地編寫一模一樣的生命周期標注。這些場景是可預測的并且遵循幾個明確的模式。接著 Rust 團隊就把這些模式編碼進了 Rust 編譯器中,如此借用檢查器在這些情況下就能推斷出生命周期而不再強制開發者顯式的增加標注。

這里我們提到一些 Rust 的歷史是因為更多的明確的模式被合并和添加到編譯器中是完全可能的。未來只會需要更少的生命周期標注。

被編碼進 Rust 引用分析的模式被稱為 生命周期省略規則(lifetime elision rules)。這并不是需要開發者遵守的規則;這些規則是一系列特定的場景,此時編譯器會考慮,如果代碼符合這些場景,就無需明確指定生命周期。

省略規則并不提供完整的推斷:如果 Rust 在明確遵守這些規則的前提下變量的生命周期仍然是模棱兩可的話,它不會猜測剩余引用的生命周期應該是什么。在這種情況,編譯器會給出一個錯誤,這可以通過增加對應引用之間相聯系的生命周期標注來解決。

函數或方法的參數的生命周期被稱為 輸入生命周期(input lifetimes),而返回值的生命周期被稱為 輸出生命周期(output lifetimes)。

編譯器采用三條規則來判斷引用何時不需要明確的標注。第一條規則適用于輸入生命周期,后兩條規則適用于輸出生命周期。如果編譯器檢查完這三條規則后仍然存在沒有計算出生命周期的引用,編譯器將會停止并生成錯誤。這些規則適用于 fn 定義,以及 impl 塊。

第一條規則是每一個是引用的參數都有它自己的生命周期參數。換句話說就是,有一個引用參數的函數有一個生命周期參數:fn foo<'a>(x: &'a i32),有兩個引用參數的函數有兩個不同的生命周期參數,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此類推。

第二條規則是如果只有一個輸入生命周期參數,那么它被賦予所有輸出生命周期參數:fn foo<'a>(x: &'a i32) -> &'a i32

第三條規則是如果方法有多個輸入生命周期參數并且其中一個參數是 &self &mut self,說明是個對象的方法(method)那么所有輸出生命周期參數被賦予self的生命周期。第三條規則使得方法更容易讀寫,因為只需更少的符號。

假設我們自己就是編譯器。并應用這些規則來計算示例中 first_word 函數簽名中的引用的生命周期。開始時簽名中的引用并沒有關聯任何生命周期:

fn first_word(s: &str) -> &str {

接著編譯器應用第一條規則,也就是每個引用參數都有其自己的生命周期。我們像往常一樣稱之為 'a,所以現在簽名看起來像這樣:

fn first_word<'a>(s: &'a str) -> &str {

對于第二條規則,因為這里正好只有一個輸入生命周期參數所以是適用的。第二條規則表明輸入參數的生命周期將被賦予輸出生命周期參數,所以現在簽名看起來像這樣:

fn first_word<'a>(s: &'a str) -> &'a str {

現在這個函數簽名中的所有引用都有了生命周期,如此編譯器可以繼續它的分析而無須開發者標記這個函數簽名中的生命周期。

讓我們再看看另一個例子,這次我們從示例 中沒有生命周期參數的 longest 函數開始:

fn longest(x: &str, y: &str) -> &str {

再次假設我們自己就是編譯器并應用第一條規則:每個引用參數都有其自己的生命周期。這次有兩個參數,所以就有兩個(不同的)生命周期:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

再來應用第二條規則,因為函數存在多個輸入生命周期,它并不適用于這種情況。再來看第三條規則,它同樣也不適用,這是因為沒有 self 參數。應用了三個規則之后編譯器還沒有計算出返回值類型的生命周期。這就是我們在嘗試編譯示例 中的代碼時出現錯誤的原因:編譯器使用所有已知的生命周期省略規則,仍不能計算出簽名中所有引用的生命周期。

方法定義中的生命周期標注

當為帶有生命周期的結構體實現方法時,其語法依然類似示例 10-11 中展示的泛型類型參數的語法。聲明和使用生命周期參數的位置依賴于生命周期參數是否同結構體字段或方法參數和返回值相關。

當為帶有生命周期的結構體實現方法時,其語法依然類似示例 中展示的泛型類型參數的語法。聲明和使用生命周期參數的位置依賴于生命周期參數是否同結構體字段或方法參數和返回值相關。

(實現方法時)結構體字段的生命周期必須總是在 impl 關鍵字之后聲明并在結構體名稱之后被使用,因為這些生命周期是結構體類型的一部分。

impl 塊里的方法簽名中,引用可能與結構體字段中的引用相關聯,也可能是獨立的。另外,生命周期省略規則也經常讓我們無需在方法簽名中使用生命周期標注。讓我們看看一些使用示例 中定義的結構體 ImportantExcerpt 的例子

首先,這里有一個方法 level。其唯一的參數是 self 的引用,而且返回值只是一個 i32,并不引用任何值:


#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a> {part: &'a str,
}impl<'a> ImportantExcerpt<'a> {fn level(&self) -> i32 {3}
}
}

impl 之后和類型名稱之后的生命周期參數是必要的,不過因為第一條生命周期規則我們并不必須標注 self 引用的生命周期。

這里是一個適用于第三條生命周期省略規則的例子:

#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a> {part: &'a str,
}impl<'a> ImportantExcerpt<'a> {fn announce_and_return_part(&self, announcement: &str) -> &str {println!("Attention please: {}", announcement);self.part}
}
}

這里有兩個輸入生命周期,所以 Rust 應用第一條生命周期省略規則并給予 &self 和 announcement 他們各自的生命周期。接著,因為其中一個參數是 &self,返回值類型被賦予了 &self 的生命周期,這樣所有的生命周期都被計算出來了。

靜態生命周期

這里有一種特殊的生命周期值得討論:'static,其生命周期能夠存活于整個程序期間。所有的字符串字面量都擁有 'static 生命周期,我們也可以選擇像下面這樣標注出來:


#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

這個字符串的文本被直接儲存在程序的二進制文件中而這個文件總是可用的。因此所有的字符串字面量都是 'static 的。

你可能在錯誤信息的幫助文本中見過使用 'static 生命周期的建議,不過將引用指定為'static之前,思考一下這個引用是否真的在整個程序的生命周期里都有效。你也許要考慮是否希望它存在得這么久,即使這是可能的。大部分情況,代碼中的問題是嘗試創建一個懸垂引用或者可用的生命周期不匹配,請解決這些問題而不是指定一個 'static 的生命周期。

結合泛型類型參數、trait bounds 和生命周期

讓我們簡要的看一下在同一函數中指定泛型類型參數、trait bounds 和生命周期的語法!


#![allow(unused)]
fn main() {
use std::fmt::Display;fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a strwhere T: Display
{println!("Announcement! {}", ann);if x.len() > y.len() {x} else {y}
}
}

這個是示例中那個返回兩個字符串 slice 中較長者的 longest 函數,不過帶有一個額外的參數 ann。ann 的類型是泛型 T,它可以被放入任何實現了 where 從句中指定的 Display trait 的類型。這個額外的參數會在函數比較字符串 slice 的長度之前被打印出來,這也就是為什么 Display trait bound 是必須的。因為生命周期也是泛型,所以生命周期參數 'a 和泛型類型參數 T 都位于函數名后的同一尖括號列表中.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/76606.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/76606.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/76606.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

藍橋杯 混乘數字

問題描述 混乘數字的定義如下&#xff1a; 對于一個正整數 n&#xff0c;如果存在正整數 a 和 b&#xff0c;使得&#xff1a; n a b且 a 與 b 的十進制數位中每個數字出現的次數之和&#xff0c;與 n 中對應數字出現的次數相同&#xff0c;則稱 n 為混乘數字。 示例 對于…

CExercise04_1位運算符_2 定義一個函數判斷給定的正整數是否為2的冪

題目&#xff1a; 給定一個正整數&#xff0c;請定義一個函數判斷它是否為2的冪(1, 2, 4, 8, 16, …) 分析&#xff1a; &#xff1a; 代碼 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <stdbool.h>/* 給定一個正整數&#xff0c;請定義一個函數…

SSL證書不可信的原因有哪些?(國科云)

SSL證書用于在客戶端和服務器端之間建立一條加密通道&#xff0c;確保數據在傳輸過程中的安全性和完整性。然而&#xff0c;在實際應用中&#xff0c;我們有時會遇到SSL證書不可信的情況&#xff0c;嚴重影響了用戶對網站的信任度。那么&#xff0c;SSL證書不可信的原因究竟有哪…

[王陽明代數講義]琴語言類型系統工程特性

琴語言類型系統工程特性 層展物理學組織實務與藝術與琴生生.物機.械科.技工.業研究.所軟凝聚態物理開發工具包社會科學氣質砥礪學人生意氣場社群成員魅力場與心氣微積分社會關系力學 意氣實體過程圖論信息編碼&#xff0c;如來碼導引 注意力機制道裝Transformer架構的發展標度律…

自抗擾ADRC之二階線性擴展狀態觀測器(LESO)推導

1.龍伯格觀測器 實際工程應用中&#xff0c;狀態變量有時難以使用傳感器直接測量&#xff0c;在這種情況下&#xff0c;使用狀態觀測器估計系統實際狀態是非常常見的做法。最出名的狀態觀測器當屬龍伯格博士在1971年發表于TAC的An Introduction to Observer[1]一文中提出的基于…

從頭開發一個Flutter插件(二)高德地圖定位插件

開發基于高德定位SDK的Flutter插件 在上一篇文章里具體介紹了Flutter插件的具體開發流程&#xff0c;從創建項目到發布。接下來將為Flutter天氣項目開發一個基于高德定位SDK的Flutter定位插件。 申請key 首先進入高德地圖定位SDK文檔內下載定位SDK&#xff0c;并按要求申請A…

分布式鎖之redis6

一、分布式鎖介紹 之前我們都是使用本地鎖&#xff08;synchronize、lock等&#xff09;來避免共享資源并發操作導致數據問題&#xff0c;這種是鎖在當前進程內。 那么在集群部署下&#xff0c;對于多個節點&#xff0c;我們要使用分布式鎖來避免共享資源并發操作導致數據問題…

ubuntu中使用安卓模擬器

本文這里介紹 使用 android studio Emulator &#xff0c; 當然也有 Anbox (Lightweight)&#xff0c; Waydroid (Best for Full Android Experience), 首先確保自己安裝了 android studio &#xff1b; sudo apt update sudo apt install openjdk-11-jdk sudo snap install…

二語習得理論(Second Language Acquisition, SLA)如何學習英語

二語習得理論&#xff08;Second Language Acquisition, SLA&#xff09;是研究學習者如何在成人或青少年階段學習第二語言&#xff08;L2&#xff09;的理論框架。該理論主要關注語言習得過程中的認知、社會和文化因素&#xff0c;解釋了學習者如何從初學者逐漸變得流利并能夠…

WinDbg. From A to Z! 筆記(下)

原文鏈接: WinDbg. From A to Z! 文章目錄 使用WinDbg臨界區相關命令示例 -- 查看臨界區其他有用的命令 WinDbg中的偽寄存器自動偽寄存器 WinDbg中的表達式其他操作默認的表達式計算方式 WinDbg中的重命名調試器命令語言編程控制流命令程序執行 WinDbg 遠程調試事件監控WinDbg …

RainbowDash 的旅行

D RainbowDash 的旅行 - 第七屆校賽正式賽 —— 補題 題目大意&#xff1a; 湖中心有一座島&#xff0c;湖的外圍有 m m m 間木屋&#xff08;圍繞小島&#xff09; &#xff0c;第 i i i 間木屋和小島之間有 a i a_i ai? 座 A A A 類橋&#xff0c; b i b_i bi? 座 B …

MySQL-SQL-DDL語句、表結構創建語句

一.SQL SQL&#xff1a;一門操作關系型數據庫的編程語言&#xff0c;定義操作所有關系型數據庫的統一標準 二. DDL-數據庫 1. 查詢所有數據庫 命令&#xff1a;show databases; 2. 查詢當前數據庫 命令&#xff1a;select database(); 3. 創建數據庫 命令&#xff1a;create da…

Sora結構猜測

方案&#xff1a;VAE Encoder&#xff08;視頻壓縮&#xff09; -> Transform Diffusion &#xff08;從視頻數據中學習分布&#xff0c;并根據條件生成新視頻&#xff09; -> VAE Decoder &#xff08;視頻解壓縮&#xff09; 從博客出發&#xff0c;經過學術Survey&am…

TortoiseSVN設置忽略清單

1.TortoiseSVN > Properties&#xff08;如果安裝了 TortoiseSVN&#xff09;。 2. 在彈出的屬性窗口中&#xff0c;點擊 New > Other。 4. 在 Property name 中輸入 svn:ignore 。 5. 在 Property value 中輸入要忽略的文件夾或文件名稱&#xff0c;例如&#xff1a; #…

深入解析Java哈希表:從理論到實踐

哈希表&#xff08;Hash Table&#xff09;是計算機科學中最重要的數據結構之一&#xff0c;也是Java集合框架的核心組件。本文將以HashMap為切入點&#xff0c;深入剖析Java哈希表的實現原理、使用技巧和底層機制。 一、哈希表基礎原理 1. 核心概念 鍵值對存儲&#xff1a;通…

leetcode:1582. 二進制矩陣中的特殊位置(python3解法)

難度&#xff1a;簡單 給定一個 m x n 的二進制矩陣 mat&#xff0c;返回矩陣 mat 中特殊位置的數量。 如果位置 (i, j) 滿足 mat[i][j] 1 并且行 i 與列 j 中的所有其他元素都是 0&#xff08;行和列的下標從 0 開始計數&#xff09;&#xff0c;那么它被稱為 特殊 位置。 示…

《數字圖像處理》教材尋找合作者

Rafael Gonzalez和Richard Woods所著的《數字圖像處理》關于濾波器的部分幾乎全錯&#xff0c;完全從零開始寫&#xff0c;困難重重。關于他的問題已經描述在《數字圖像處理&#xff08;面向新工科的電工電子信息基礎課程系列教材&#xff09;》。 現尋找能夠共同討論、切磋、…

為 Jenkins Agent 添加污點(Taint)容忍度(Toleration)

在 Kubernetes&#xff08;k8s&#xff09;環境中使用 Jenkins 時&#xff0c;為 Jenkins Agent 添加污點&#xff08;Taint&#xff09;容忍度&#xff08;Toleration&#xff09;是一種常見的配置操作&#xff0c;它允許 Jenkins Agent Pod 被調度到帶有特定污點的節點上。下…

LeetCode算法題(Go語言實現)_28

題目 Dota2 的世界里有兩個陣營&#xff1a;Radiant&#xff08;天輝&#xff09;和 Dire&#xff08;夜魘&#xff09; Dota2 參議院由來自兩派的參議員組成。現在參議院希望對一個 Dota2 游戲里的改變作出決定。他們以一個基于輪為過程的投票進行。在每一輪中&#xff0c;每一…

使用python實現視頻播放器(支持拖動播放位置跳轉)

使用python實現視頻播放器&#xff08;支持拖動播放位置跳轉&#xff09; Python實現視頻播放器&#xff0c;在我早期的博文中介紹或作為資料記錄過 Python實現視頻播放器 https://blog.csdn.net/cnds123/article/details/145926189 Python實現本地視頻/音頻播放器https://bl…