泛型是具体类型或其他属性的抽象替代。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。
之后,我们讨论 trait,这是一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型。
最后介绍 生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。
提取函数来减少重复
在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数。当熟悉了这个技术以后,我们将使用相同的机制来提取一个泛型函数!如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码。
考虑一下这个寻找列表中最大值
fn main() {
let num = vec![12, 34, 45, 32, 15, 89];
let mut largest = num[0];
for number in num {
if number > largest {
largest = number;
}
}
println!("the largest number is {}", largest);
}
如果需要在两个不同的列表中寻找最大值,我们可以重复上述代码
为了消除重复,我们可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数。这将增加代码的简洁性并让我们将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立。
fn find_largest(num: Vec<i32>) -> i32{
let mut largest = num[0];
for number in num {
if number > largest {
largest = number;
}
}
largest
}
fn main() {
let num = vec![12, 34, 45, 32, 15, 89];
let data = find_largest(num);
println!("the largest number is {}", data);
let num = vec![120, 304, 405, 320, 105, 89];
let data = find_largest(num);
println!("the largest number is {}", data);
}
find_largest
函数有一个参数 num
,它代表会传递给函数的任何具体的 i32
值的 slice。函数定义中的 num
代表任何 &[i32]
。当调用 find_largest
函数时,其代码实际上运行于我们传递的特定值上。
总的来说,经历了如下几步:
- 找出重复代码。
- 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
- 将重复代码的两个实例,改为调用函数。
在不同的场景使用不同的方式,我们也可以利用相同的步骤和泛型来减少重复代码。
10.1 泛型数据类型
在函数定义中使用泛型
当使用泛型定义函数时,我们在函数签名中通常为参数和返回值指定数据类型的位置放置泛型。以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码。
回到 largest
函数上,示例 中展示了两个提供了相同的寻找 slice 中最大值功能的函数。
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');
}
这两个函数有着相同的代码,所以让我们在一个单独的函数中引入泛型参数来消除重复。
当需要在函数体中使用一个参数时,必须在函数签名中声明这个参数以便编译器能知道函数体中这个名称的意义。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。为了定义泛型版本的 largest
函数,类型参数声明位于函数名称与参数列表中间的尖括号 <>
中,像这样:
fn largest<T>(list: &[T]) -> T {
这可以理解为:函数 largest
有泛型类型 T
。它有一个参数 list
,它的类型是一个 T
值的 slice。largest
函数将会返回一个与 T
相同类型的值。
// 注意这里的函数签名(形参、实参)
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);
assert_eq!(result, 100);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
assert_eq!(result, 'y');
}
如果现在就尝试编译这些代码,会出现如下错误:
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
注释中提到了 std::cmp::PartialOrd
,这是一个 trait。下一部分会讲到 trait。不过简单来说,这个错误表明 largest
的函数体不能适用于 T
的所有可能的类型。因为在函数体需要比较 T
类型的值,不过它只能用于我们知道如何排序的类型。为了开启比较功能,标准库中定义的 std::cmp::PartialOrd
trait 可以实现类型的比较功能。
结构体定义中的泛型
同样也可以使用 <>
语法来定义拥有一个或多个泛型参数类型字段的结构体。
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
fn main() {
let point1 = Point{x: 4, y: 6};
let point2 = Point{x: 'a', y: 'b'};
println!("{:#?}", point1);
println!("{:#?}", point2);
}
字段 x
和 y
必须是相同类型,因为他们都有相同的泛型类型 T
在这个例子中,当把整型值 5 赋值给 x
时,就告诉了编译器这个 Point<T>
实例中的泛型 T
是整型的。接着指定 y
为 4.0,它被定义为与 x
相同类型,就会得到一个像这样的类型不匹配错误:
如果想要定义一个 x
和 y
可以有不同类型且仍然是泛型的 Point
结构体,我们可以使用多个泛型类型参数。
#[derive(Debug)]
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let point1 = Point{x: 5, y: 4.0};
let point2 = Point{x: 'a', y: 'b'};
println!("{:#?}", point1);
println!("{:#?}", point2);
}
枚举定义中的泛型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。
enum Option<T> {
Some(T),
None,
}
现在这个定义看起来就更容易理解了。如你所见 Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option<T>
枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型。
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚举有两个泛型类型,T
和 E
。Result
有两个成员:Ok
,它存放一个类型 T
的值,而 Err
则存放一个类型 E
的值。这个定义使得 Result
枚举能很方便的表达任何可能成功(返回 T
类型的值)也可能失败(返回 E
类型的值)的操作。
方法定义中的泛型
也可以在定义中使用泛型在结构体和枚举上实现方法
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
,它返回 T
类型的字段 x
的引用
注意必须在 impl
后面声明 T
,这样就可以在 Point<T>
上实现的方法中使用它了。在 impl
之后声明泛型 T
,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。下例中在示例 中的结构体 Point<T, U>
上定义了一个方法 mixup
。这个方法获取另一个 Point
作为参数,而它可能与调用 mixup
的 self
是不同的 Point
类型。这个方法用 self
的 Point
类型的 x
值(类型 T
)和参数的 Point
类型的 y
值(类型 W
)来创建一个新 Point
类型的实例:
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
。
略复杂,但是万变不离其宗
泛型代码的性能
Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并没有任何速度上的损失。
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option<T>
展开为 Option_i32
和 Option_f64
,接着将泛型定义替换为这两个具体的定义。
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);
}
10.2 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;
}
fn main() {
}
这里使用 trait
关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 Summary
。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是 fn summarize(&self) -> String
。
在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 trait 的类型都需要提供其自定义行为的方法体,编译器也会确保任何实现 Summary
trait 的类型都拥有与这个签名的定义完全一致的 summarize
方法。
抽象类?
trait 体中可以有多个方法:一行一个方法签名且都以分号结尾。
为类型实现 trait
现在我们定义了 Summary
trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上实现它了。
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 实现这个triat
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,
}
// 实现这个triat
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
}
在类型上实现 trait 类似于实现与 trait 无关的方法。区别在于 impl
关键字之后,我们提供需要实现 trait 的名称,接着是 for
和需要实现 trait 的类型的名称。在 impl
块中,使用 trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 trait 方法所拥有的行为。
调用方法
fn main() {
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 的类型位于 crate 的本地作用域时,才能为该类型实现 trait。但是不能为外部类型实现外部 trait。
默认实现
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
如果想要对 NewsArticle
实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {}
指定一个空的 impl
块。
虽然我们不再直接为 NewsArticle
定义 summarize
方法了,但是我们提供了一个默认实现并且指定 NewsArticle
实现 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
了,而 summary
的默认实现会调用我们提供的 summarize_author
定义。因为实现了 summarize_author
,Summary
trait 就提供了 summarize
方法的功能,且无需编写更多的代码。
完整代码
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// 实现这个triat
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
fn main() {
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 来接受多种不同类型的参数。
例如在示例中为 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
。
完整代码
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// 实现这个triat
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
// 注意形参的表达形式
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};
// 调用
notify(tweet);
}
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
值的具体类型必须一致。
完整代码
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// 实现这个triat
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
// 注意形参的表达形式
pub fn notify<T: Summary>(item :T){
println!("Breaking news! {}", item.summarize());
}
fn main() {
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());
notify(tweet);
}
通过+指定多个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) -> i32
where T: Display + Clone,
U: Clone + Debug
{
好复杂,好细节
返回实现了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
,不过调用方并不知情。
不过这只适用于返回单一类型的情况。例如,这段代码的返回值类型指定为返回 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 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,
}
}
}
使用trait bounds来修复largest函数
现在你知道了如何使用泛型参数 trait bound 来指定所需的行为。让我们回到前面示例修复使用泛型类型参数的 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);
}
最后的错误是
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
我的是
将 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
。像 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 的类型实现方法。例如,下面中的类型 Pair<T>
总是实现了 new
方法,不过只有那些为 T
类型实现了 PartialOrd
trait (来允许比较) 和 Display
trait (来启用打印)的 Pair<T>
才会实现 cmp_display
方法:
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 标准库中。
trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
10.3 生命周期与引用有效性
Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
生命周期避免了悬垂引用
生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。
fn main()
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
外部作用域声明了一个没有初值的变量 r
,而内部作用域声明了一个初值为 5 的变量x
。在内部作用域中,我们尝试将 r
的值设置为一个 x
的引用。接着在内部作用域结束后,尝试打印出 r
的值。这段代码不能编译因为 r
引用的值在尝试使用之前就离开了作用域。
结果
变量 x
并没有 “存在的足够久”。其原因是 x
在到达第 9 行内部作用域结束时就离开了作用域。不过 r
在外部作用域仍是有效的;作用域越大我们就说它 “存在的越久”。
借用检查器
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
要小:被引用的对象比它的引用者存在的时间更短。
并没有产生悬垂引用且可以正确编译的例子:
fn main() {
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
}
这里 x
拥有生命周期 'b
,比 'a
要大。这就意味着 r
可以引用 x
:Rust 知道 r
中的引用在 x
有效的时候也总是有效的。
函数中的泛型生命周期
写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。
fn longest(x: &str, y: &str) -> &str {
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);
}
注意这个函数获取作为引用的字符串 slice,因为我们不希望 longest
函数获取参数的所有权。我们期望该函数接受 String
的 slice(参数 string1
的类型)和字符串字面值(包含于参数 string2
)
结果
提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 x
或 y
。事实上我们也不知道,因为函数体中 if
块返回一个 x
的引用而 else
块返回一个 y
的引用!
当我们定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 if
还是 else
会被执行。我们也不知道传入的引用的具体生命周期,所以也就不能像示例 10-18 和 10-19 那样通过观察作用域来确定返回的引用是否总是有效。借用检查器自身同样也无法确定,因为它不知道 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 关于参数中的引用和返回值之间的限制是他们都必须拥有相同的生命周期,
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("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
现在函数签名表明对于某些生命周期 'a
,函数会获取两个参数,他们都是与生命周期 'a
存在的一样长的字符串 slice。函数会返回一个同样也与生命周期 'a
存在的一样长的字符串 slice。这就是我们告诉 Rust 需要其保证的契约。记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入后返回的值的生命周期。而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。注意 longest
函数并不需要知道 x
和 y
具体会存在多久,而只需要知道有某个可以被 'a
替代的作用域将会满足这个签名。
当具体的引用被传递给 longest
时,被 'a
所替代的具体生命周期是 x
的作用域与 y
的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a
的具体生命周期等同于 x
和 y
的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a
标注了返回的引用值,所以返回的引用值就能保证在 x
和 y
中较短的那个生命周期结束之前保持有效。
深入理解生命周期
指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 longest
函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y
指定一个生命周期。如下代码将能够编译:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
在这个例子中,我们为参数 x
和返回值指定了生命周期参数 'a
,不过没有为参数 y
指定,因为 y
的生命周期与参数 x
和返回值的生命周期没有任何关系。
当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
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 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
结构体定义中的生命周期注解
接下来,我定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。下面示例中有一个存放了一个字符串 slice 的结构体
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)
定义了一个没有使用生命周期注解的函数,即便其参数和返回值都是引用
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 引用分析的模式被称为 生命周期省略规则(lifetime elision rules)。这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期。
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。
函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。
编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。
第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是,有一个引用参数的函数有一个生命周期参数: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)(译者注: 这里涉及rust的面向对象参见17章), 那么所有输出生命周期参数被赋予 self
的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。
假设我们自己就是编译器。开始时签名中的引用并没有关联任何生命周期:
fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员标记这个函数签名中的生命周期。
让我们再看看另一个例子,这次我们从示例 10-21 中没有生命周期参数的 longest
函数开始:
fn longest(x: &str, y: &str) -> &str {
再次假设我们自己就是编译器并应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 self
参数。应用了三个规则之后编译器还没有计算出返回值类型的生命周期。这就是为什么在编译示例的代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。
方法定义中的生命周期注解
(实现方法时)结构体字段的生命周期必须总是在 impl
关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。
impl
块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例 10-25 中定义的结构体 ImportantExcerpt
的例子。
首先,这里有一个方法 level
。其唯一的参数是 self
的引用,而且返回值只是一个 i32
,并不引用任何值:
fn main() {
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
}
impl
之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 self
引用的生命周期。
静态生命周期
这里有一种特殊的生命周期值得讨论:'static
,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static
生命周期,我们也可以选择像下面这样标注出来:
fn main() {
let s: &'static str = "I have a static lifetime.";
}
这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static
的。
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
结合泛型类型参数、trait bounds和生命周期
要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法!
fn main() {
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where 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
都位于函数名后的同一尖括号列表中。
参考:泛型、trait 与生命周期 - Rust 程序设计语言 简体中文版 (bootcss.com)