参考:
- https://doc.rust-lang.org/stable/book/ch10-00-generics.html
- https://doc.rust-lang.org/rust-by-example/generics.html
泛型 (generics):是具体类型或其他属性的抽象替代,是 Rust 中高效处理重复概念的工具之一。我们可以表达泛型的属性,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么。泛型类型参数意味着代码可以适用于不同的类型。泛型帮助我们确保类型拥有期望的行为。
trait :一个定义泛型行为的方法。trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型,这个技术被称为 trait bound。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。
生命周期 (lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。生命周期帮助确保引用在我们需要他们的时候一直有效。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。
以上所有的一切发生在编译时所以不会影响运行时效率!
你可能不会相信,这个话题还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。
泛型
如同你识别出可以提取到函数中重复代码那样,你也能够使用泛型来处理重复代码。提取函数来减少重复可以分成三个步骤:
- 找出重复代码。
- 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值。
- 将重复代码的两个实例,改为调用函数。
泛型的作用:使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
当你意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。
为了参数化新函数中的这些类型,我们也需要为类型参数取个名字,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 T
,因为传统上来说,Rust 的参数名字都比较短,通常就只有一个字母,同时,Rust 类型名的命名规范是骆驼命名法(UpperCamelCase)。T
作为 “type” 的缩写是大部分 Rust 程序员的首选。
当你的代码中需要许多泛型类型时,它可能表明你的代码需要重构,分解成更小的结构。
定义泛型
泛型放在 <>
内:
// 函数中定义的泛型
fn largest<T>(list: &[T]) -> T {}
// 结构体中定义的泛型
// 只使用了一个泛型类型,这个定义表明结构体 Point<T> 对于类型 T 是泛型的
// 而且字段 x 和 y 都是 相同类型的,无论它具体是何类型
struct Point<T> { x: T, y: T }
// x 和 y 可以有不同类型且仍然是泛型
struct Point<T, U> { x: T, y: U }
// 枚举体中定义的泛型
enum Option<T> { Some(T), None }
enum Result<T, E> { Ok(T), Err(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());
}
注意必须在 impl
后面声明 T
,这样就可以在 Point<T>
上实现的方法中使用它了。在 impl
之后声明泛型 T
,这样 Rust 就知道 Point
的尖括号中的类型是泛型而不是具体类型。如果不在 impl
后面声明 T
,我们可以对具体类型实现方法:
impl Point<f32> {
// 只对 f32 类型的两点计算距离
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
再来看一个复杂一些的例子:
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); // p3.x = 5, p3.y = c
}
这里泛型参数 T
和 U
声明于 impl
之后,因为它们与结构体定义相对应。而泛型参数 V
和 W
声明于 fn mixup
之后,因为它们只是相对于方法本身的。
使用泛型
- 隐式使用泛型:
- 实例化结构体或者枚举体时,传入具体类型的值
- 在函数参数的位置上,传入具体类型的值
- 显式使用泛型:
结构体名称<具体类型>
、枚举体名称<具体类型>
函数名::<具体类型>
泛型结构体和泛型函数的综合使用:
//src: https://doc.rust-lang.org/rust-by-example/generics/gen_fn.html
struct A; // Concrete type `A`.
struct S(A); // Concrete type `S`.
struct SGen<T>(T); // Generic type `SGen`.
// The following functions all take ownership of the variable passed into
// them and immediately go out of scope, freeing the variable.
// Define a function `reg_fn` that takes an argument `_s` of type `S`.
// This has no `<T>` so this is not a generic function.
fn reg_fn(_s: S) {}
// Define a function `gen_spec_t` that takes an argument `_s` of type `SGen<T>`.
// It has been explicitly given the type parameter `A`, but because `A` has not
// been specified as a generic type parameter for `gen_spec_t`, it is not generic.
fn gen_spec_t(_s: SGen<A>) {}
// Define a function `gen_spec_i32` that takes an argument `_s` of type `SGen<i32>`.
// It has been explicitly given the type parameter `i32`, which is a specific type.
// Because `i32` is not a generic type, this function is also not generic.
fn gen_spec_i32(_s: SGen<i32>) {}
// Define a function `generic` that takes an argument `_s` of type `SGen<T>`.
// Because `SGen<T>` is preceded by `<T>`, this function is generic over `T`.
fn generic<T>(_s: SGen<T>) {}
fn main() {
// Using the non-generic functions
reg_fn(S(A)); // Concrete type.
gen_spec_t(SGen(A)); // Implicitly specified type parameter `A`.
gen_spec_i32(SGen(6)); // Implicitly specified type parameter `i32`.
// Explicitly specified type parameter `char` to `generic()`.
generic::<char>(SGen('a'));
// Implicitly specified type parameter `char` to `generic()`.
generic(SGen('c'));
}
给泛型结构体实现方法:
struct S; // Concrete type `S`
struct GenericVal<T>(T); // Generic type `GenericVal`
// impl of GenericVal where we explicitly specify type parameters:
impl GenericVal<f32> {} // Specify `f32`
impl GenericVal<S> {} // Specify `S` as defined above
// `<T>` Must precede the type to remain generic
impl<T> GenericVal<T> {}
枚举体使用泛型:
#[derive(Debug)]
enum A<T> {
A1(T),
A2(i32),
}
fn main() {
let a: A<u32> = A::A1(1);
dbg!(a); // A1(1)
let a: A<f32> = A::A2(1);
dbg!(a); // A2(1)
}
泛型的效率
Rust 使用泛型类型参数的代码相比使用具体类型并没有任何(运行时)速度上的损失。
Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证(运行)效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程(因此编译时需要更多时间,参考:Rust 编译模型之殇)。这个过程举例来说:
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option<T>
展开为 Option_i32
和 Option_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 指定泛型是任何拥有特定行为的类型。类似于其他语言中的常被称为 接口 (interfaces )的功能,虽然有一些不同。
一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
定义和实现 trait
- 使用
trait 名称
声明,内部是方法签名fn 名称(&self, ...) -> 返回类型;
实现 trait:
impl trait名称 for 特定类型
,对声明好的方法签名编写函数体,进行具体功能的实现。也就是把 trait 应用在类型上。注意:- 方法的具体实现必须与签名一致,即输入参数和类型、返回类型需要一致。
- impl trait 的时候必须把 trait 声明的全部方法都提供具体实现。
// 声明 trait,加 `pub` 的目的是让其成为公有 trait 使得其他 crate 可以实现(应用)它
pub trait Summary {
fn summarize(&self) -> String; // 声明方法签名,不使用 `{}`,直接以 `;` 结束
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 对一个结构体实现 trait
impl Summary for NewsArticle {
// 使用 trait 定义中的方法签名,编写函数体来为特定类型实现 trait 方法所拥有的行为(功能)
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,
}
// 对另一个结构体实现 trait
impl Summary for Tweet {
// 同一个方法在不同类型上有不同的行为(功能)
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
在其他 crate 中使用和实现 trait:把 trait 引入作用域即可。比如上述定义的
Summary trait
在 aggregator crate,在另一个 crate 中的代码:use aggregator::Summary;
impl Summary for SomeType {
fn summarize(&self) -> String {
// todo
}
}
孤儿原则 (orphan rule):只有当 trait 或者要实现 trait 的类型位于 crate 的本地作用域时,才能为该类型实现 trait,即不能为外部类型实现外部 trait。
- 首先必须谨记,要实现 trait,势必是 “针对特定类型”(具体类型或者泛型),即实现 trait 时 trait 和特定类型是挂钩的;
- 其次,要么 trait 是在当前 crate 被定义,要么 打算实现的特定类型 是在当前 crate 被定义,才能实现 trait (给特定类型编写 trait 下的方法的具体功能);
- 如果 trait 和特定类型 都不是当前 crate 定义的话,当前 crate 是不能实现这个 trait 的。
举例来说:
- 当前处于 aggregator crate ,
Summary trait
和NewsArticle
、Tweet
两个结构体都在本地作用域,所以这两个结构体完全可以实现Summary trait
下的具体方法。也可以给外部的 crate,比如 标准库 的类型实现Summary trait
,因为 trait 是自己定义的。 - 当前处于 outer crate,调用 aggregator crate,针对定义在后者的
Summary trait
,outer crate 只能给自己内部编写的类型实现Summary trait
,而不能去给后者里定义的任何类型实现或者覆盖Summary trait
,因为 trait 和类型都是外部的。 - 没有孤儿原则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。因为重载一个默认实现的语法与实现没有默认实现的 trait 方法的语法一样。如果允许给外部的类型实现外部的 trait,很可能会把其他人编写的代码破坏掉:就好比你定义的类型的方法竟然可以被调用者修改。因此,孤儿原则确保了代码的安全性。
“绕开”孤儿原则的 workaround:”newtype 模式” 。
默认实现 与 重载实现
默认实现 defualt implementations:
有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。
这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。
有两种提供默认实现的方式:在声明 trait 的时候就提供实现的方法,在 impl tarit 的时候:
- 如果不再提供方法,就调用 trait 的默认方法;
- 如果重新提供方法,就覆盖 trait 的默认方法,这被称为 重载实现 (override implementations),默认方法不再被调用。
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());
// 默认实现时打印:1 new tweet: (Read more ...)
// 重载实现时打印:1 new tweet: horse_ebooks: of course, as you probably already know, people...
}
pub trait Summary {
fn summarize(&self) -> String {
format!("(Read more ...)")
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
// 不提供方法,就调用 trait 的默认方法
impl Summary for Tweet {}
// 或者提供方法,重载 implementations
// impl Summary for Tweet {
// fn summarize(&self) -> String {
// format!("{}: {}", self.username, self.content)
// }
// }
在声明 trait 的时候提供一个实现的方法,而且提供的这个默认方法预先调用该 trait 的其他方法;在 impl tarit 的时候对那些预先调用的方法提供具体实现。
因为默认实现允许调用相同 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());
// 打印结果:1 new tweet: (Read more from @horse_ebooks...)
}
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,
}
impl Summary for Tweet {
// summarize 已经是默认的方法
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
这里的 summarize
方法依然可以被重载,只需要写出符合 method 签名新的功能实现即可:
impl Summary for Tweet {
// 这里不再调用 summarize_author,默认实现被重载
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
// 即使 summarize 不需要调用,impl trait 的时候必须提供所有 method
// 所以 summarize_author 是必要的
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
被重载实现的打印结果:
1 new tweet: horse_ebooks: of course, as you probably already know, people...
作为函数签名的类型
trait 虽然不是一种类型,但它表示泛型的一种行为,把它作为函数的类型参数,表示这个函数参数接收具有实现该 trait 的类型变量,从而可以直接在函数体调用 trait 的方法。
把 trait 作为输入参数的类型。
pub fn notify_move(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify_borrow(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify_borrow_mut(item: &mut impl Summary) {
println!("Breaking news! {}", item.summarize());
}
这个例子定义一系列函数
notify
来调用其参数item
上的summarize
方法,该参数是实现了Summary
trait 的某种类型,细微的区别在于对参数所有权的处理。
对于item
参数,我们指定了impl
关键字和 trait 名称,而不是具体的类型。该参数支持任何实现了指定 trait 的类型。在notify
函数体中,可以调用任何来自Summary
trait 的方法,比如summarize
。我们可以传递任何NewsArticle
或Tweet
的实例来调用notify
。任何用其它如String
或i32
的类型调用该函数的代码都不能编译,因为它们没有实现Summary
。把 trait 作为返回值的类型。
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
...
}
}
返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用,#todo:闭包和迭代器# 闭包和迭代器创建只有编译器知道的类型,或者是非常非常长的类型。
impl Trait
允许你简单的指定函数返回一个Iterator
而无需写出实际的冗长的类型。
然而,如果函数尝试可能返回同时具有 trait 但不同的数据类型时,编译器不会通过代码,比如:// 无法运行
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
...
}
} else {
Tweet {
username: String::from("horse_ebooks"),
...
}
}
}
这里尝试返回
NewsArticle
或Tweet
。这不能编译,因为impl Trait
工作方式的限制。可以为使用不同类型的值而设计的 trait 对象来实现相同的需求:fn returns_summarizable(switch: bool) -> Box<dyn Summary> {
if switch {
Box::new(NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
...
})
} else {
Box::new(Tweet {
username: String::from("horse_ebooks"),
...
})
}
}
当然,我们也可以使用 枚举体 来避开 trait 作为参数泛型:
trait Summary {
fn say_hi(&self) -> String { "hi".to_string() }
}
impl Summary for Tweet {}
impl Summary for NewsArticle {}
#[derive(Debug)]
struct Tweet {
username: String,
}
#[derive(Debug)]
struct NewsArticle {
headline: String,
}
#[derive(Debug)]
enum Media {
Tweet(Tweet),
NewsArticle(NewsArticle),
}
fn main() {
let summary_chosen = returns_summarizable(true); // summary_chosen: Media
println!("{:?}", summary_chosen);
match summary_chosen {
Media::NewsArticle(_) => println!("News"),
Media::Tweet(_) => println!("Tweet"),
}
}
fn returns_summarizable(switch: bool) -> Media {
if switch {
Media::NewsArticle(NewsArticle{
headline: String::from("Penguins win the Stanley Cup Championship!"),
})
} else {
Media::Tweet(Tweet { username: String::from("horse_ebooks") })
}
}
trait bound 语法
Rust 提供了 trait bound 语法,来把 trait 参数类型看作泛型:
pub fn notify(item: impl Summary) { }
// 使用 trait bound 语法的等价写法
pub fn notify<T: Summary>(item: T) { }
impl Trait
很方便,适用于短小的例子。trait bound 则适用于更复杂的场景。
例如,以下获取两个实现了 Summary
的参数。
使用 impl Trait
和 trait bound
语法,这适用于 item1
和 item2
允许是不同类型的情况(只要它们都实现了 Summary
):
// impl Trait 语法
pub fn notify(item1: impl Summary, item2: impl Summary) { }
// trait bound 语法
pub fn notify<T: Summary, U: Summary>(item1: T, item2: U) { }
如果指定参数 item1
和 item2
值的具体类型必须一致,则使用 trait bound
语法才可能做到:
pub fn notify<T: Summary>(item1: T, item2: T) { }
通过
+
指定多个 trait:// item 参数的类型必须同时实现 Summary 和 Display trait
pub fn notify(item: impl Summary + Display) { }
// trait bound 语法
pub fn notify<T: Summary + Display>(item: T) { }
通过 where 简化 trait bound:
// 每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间
// 会有很长的 trait bound 信息,这使得函数签名难以阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 { }
// 在函数签名之后的 where 从句中指定 trait bound 的语法
// 使得 函数名、参数列表和返回值类型都离得很近,函数签名就显得不那么杂乱
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{ }
利用 trait bound 语法,在 “给泛型实现方法时” 加上 trait 作为泛型的限定条件:
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 }
}
}
// 在实现方法时,利用 trait bound 给泛型添加条件
// 此处的方法仅实现给具有 Display 和 PartialOrd trait 的类型
// 即让具有比较大小和打印输出功能的 field,在实例中具备 cmp_display 方法
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);
}
}
}
fn main() {
let pai_u32: Pair<u32> = Pair::new(0, 1);
pair_u32.cmp_display();
let pair_string = Pair { x: "abc", y: "aba" };
pair_string.cmp_display();
}
可以对任何实现了特定 trait 的类型有条件地实现 trait。
听起来有点绕,其实就是在impl SomeTrait for SomeType
“的时候”,对这里的 SomeType 利用 trait bound 语法来限定条件,因而形式上就是impl<SomeType: BTrait> ATrait for SomeType
。
实现了特定 trait 的类型 就是指 实现 BTrait 的 SomeType;条件地 就是指 使用<SomeType: BTrait>
语法,当然包括使用+
或where
语法;实现 trait 就是指 整个语句所完成的事情。
下面是来自标准库的例子和实际应用:
// ToString 是一个 trait, T 是泛型
// 所以这个 trait implementation 意味着
// 给任何实现了 Display trait 的类型实现 ToString trait
impl<T: Display> ToString for T {
// --snip--
}
// 因为整型实现了 Display
// 从而可以使用 ToString trait 的 to_string 方法将其转化成 String
let s = 3.to_string();
上面这种形式的 impl
块有专门的称呼:对任何满足特定 trait bound 的类型 实现 trait 被称为 blanket implementations ,他们被广泛的用于 Rust 标准库中。
即 blanket implementations 是:
- 功能上看就是 trait implementations (给类型实现 trait)
- 而 blanket 作为形容词表示“包括所有情形的、无一例外的”,所以这个词描述了这样的事实: trait bound 限定的类型(
BTrait
)都毫无例外地被实现了新的 trait (ATrait
) - 中文可翻译为“ ”
- 对 trait 施加 trait bound,见 “高级 trait -> supertrait” :比较对泛型和对 trait 施加 trait bound 的语法
// trait bound: <T: trait>
impl<T: Supertrait> Subtrait for T { }
// where T: Supertrait
impl<T> Subtrait for T where T: Supertrait { }
// Subtrait: Supertrait
trait Subtrait: Supertrait { }
// where Self: Supertrait
trait Subtrait where Self: Supertrait { } /* 这里的 Self 指 Subtrait */
总之,trait 和 trait bound 让我们使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为我们向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为。
在动态类型语言中,如果我们尝试调用一个类型并没有实现的方法,会在运行时出现错误。Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫我们修复错误。另外,我们也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能。
从函数抽象到泛型+trait抽象
largest 例子:在 [i32]
或者 [char]
甚至 [str]
中通过 比较 得到“最大”的元素。
- 如果不使用抽象,我们需要对不同的具体类型编写重合度很高的代码。因为除了比较的方式不一样之外,它们都要经历从一个初始值开始与其他所有值比较的过程。
- 如果使用函数抽象,我们可以简化一些流程上代码,但是针对不同的元素类型需要编写不同的“比较”代码,毕竟数字和字符比较不同,字符和字符串比较的逻辑也略有不同。
- 如果使用泛型抽象,我们可以最大化简化代码,构造出最精简的框架。那些略为不同细节只需要利用 trait:给不同类型实现各自的 trait 方法,但是利用同一个 trait 名称来表明某种意义上它们共性。
以下直接过渡到泛型抽象。需要清楚,使用大于运算符(>
)比较两个 T
类型的值的背后原理是:这个运算符被定义为标准库中 trait std::cmp::PartialOrd
的一个默认方法。而且 PartialOrd
这个 trait 被 preluded,所以并不需要手动将其引入作用域。
// PartialOrd trait 让 largest 函数可以用于任何可以比较大小的类型的 slice
// Copy trait 让 list[0] 赋给变量值的时候使用 Copy 而不是 move,因为 `&` 只能被 borrow
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0]; // largest 获得了 array 元素的所有权,即所有权被转交出去 (move)
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
// 因为不是所有类型都能实现 Copy trait(比如大小在编译时无法确定的那些类型)
// 所以考虑 Clone trait 来手动处理赋值时候的所有权问题:
// 克隆 slice 的每一个值使得 largest 函数拥有其所有权
// 使用 clone 函数意味着对于类似 String 这样拥有堆上数据的类型,会潜在的分配更多堆上空间
// 而堆分配在涉及大量数据时可能会相当缓慢
fn largest<T: PartialOrd + Clone>(list: &[T]) -> T {
let mut largest = list[0].clone(); // largest 获得了 array 元素的 clone
for item in list.clone() {
if *item > largest {
largest = item.clone();
}
}
largest
}
// 注意 Copy / Clone trait 被添加在泛型上的原因在于处理赋值时的所有权问题
// 如果我们将函数返回值从 T 改为 &T ,使其能够返回一个(不可变)引用
// 那么就可以绕过数据到底应该被 move 还是 copy 的问题
// 即不可变引用让我们将不需要任何 Clone 或 Copy 的 trait bounds 而且也不会有任何的堆分配
fn largest<T: PartialOrd>(list: &[T]) -> &T { // 联想起生命周期省略规则 2
let mut largest = &list[0]; // largest 获得了 array 元素的引用
for item in list {
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); // 100
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result); // y
let char_list = vec!["aaa", "bb", "c"];
let result = largest(&char_list);
println!("The largest char is {}", result); // "c"
}
lifetime
Rust 中的每一个引用都有其 生命周期 (lifetime ),也就是引用保持有效的作用域。所以使用引用的时候,尤其在传递引用的时候必须关注生命周期。生命周期的概念从某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能。
大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。
引用的生命周期
生命周期的主要目标是避免悬垂引用,悬垂引用会导致程序引用了非预期引用的数据。Rust 编译器有一个 借用检查器 (borrow checker ),它比较作用域来确保所有的借用都是有效的。
// 设想一下,r 和 x 的生命周期注解,分别叫做 'a 和 'b
// 在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象
// 生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短,所以程序被拒绝编译,因而避免了悬垂引用
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
// 一个有效的引用,因为数据比引用有着更长的生命周期
// x 拥有生命周期 'b,比 'a 要大,这就意味着 r 可以引用 x
// Rust 知道 r 中的引用在 x 有效的时候也总是有效的
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
引用的生命周期语法:
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
像上面这样单个的生命周期注解本身没有多少意义。
生命周期注解的意义在于:告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。
Lifetime Elision 生命周期省略
规则 (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
可以被省略成fn foo(x: &i32) -> &i32
。 - 对于输出生命周期(真正能够适用的就只有 method 签名):如果 method 有多个输入生命周期参数并且其中一个参数是
&self
或&mut self
,说明是个对象的方法 (method) , 那么所有输出生命周期参数被赋予self
的生命周期。这条规则使得 method 更容易读写,因为只需更少的符号。#todo: Rust 的面向对象#
省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决。
函数签名中的生命周期注解
在函数中使用生命周期注解的具体意义是:将函数的多个 参数 与其 返回值 的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
例如如果函数有一个生命周期 'a
的 str
的引用的参数 first
。还有另一个同样是生命周期 'a
的 str
的引用的参数 second
。这两个生命周期注解意味着引用 first
和 second
必须与这泛型生命周期存在得一样久。反映在函数签名上就是:
// 就像泛型类型参数,泛型生命周期参数需要声明在函数名和参数列表间的尖括号中
fn f<'a>(first: &'a str, second: &'a str) -> &'a str{ }
// 当然也允许部分参数使用生命周期注解:
// 以下这条函数签名表明 返回值必须和参数 x 的声明周期一样久;
// 如果实际传入的引用先于返回值 drop,此处代码将被编译器拒绝。
fn f<'a>(x: &'a str, y: &str) -> &'a str { }
// === 下面两种方式是不能被编译的 ===
// 使用 <'a> 声明了函数的生命周期之后,不允许返回值没有生命周期注解。
// 因为如果返回值不需要生命周期注解,意味着编译器能自动推断,那么就不用给函数声明 <'a>
fn f<'a>(first: &'a str, second: &'a str) -> &str{ }
// 使用 <'a> 声明了函数的生命周期之后,不允许所有参数没有生命周期注解。
// 因为如果返回的引用没有指向任何一个参数(函数的外部数据)
// 那么唯一的可能就是它指向一个函数内部创建的值(函数的内部数据)
// 所以返回值将会是一个悬垂引用,因为它将会在函数结束时离开作用域
fn f<'a>(x: &str, y: &str) -> &'a str { // 返回值的生命周期与参数完全没有关联
let result = String::from("really long string");
result.as_str() // 返回值是函数内部创建的数据引用
} // result.as_str() 在这里离开作用域并被清理,所以返回了悬垂(无效)引用
// 在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样就不用考虑生命周期问题了
一个经典的错误:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2); // 注意 str 是 slice 类型,和引用一样是没有所有权概念的
println!("The longest string is {}", result);
}
// 借用检查器无法确定 x 和 y 的生命周期是如何与返回值的生命周期相关联的
// 因为定义这个函数的时候,并不知道传递给函数的具体值,所以也不知道到底是 if 还是 else 会被执行
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x // 执行 if 时,x 被返回
} /*而 y 在此处离开作用域并被清理*/ else {
y // 执行 else 时,y 被返回
} // 而 x 在此处离开作用域并被清理
}
// 为了修复这个错误,增加泛型生命周期参数来定义引用间的关系以便借用检查器可以进行分析
// 现在函数签名表明对于某些生命周期 'a,函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串 slice
// 函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice
// longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。这就是我们告诉编译器需要其保证的约束条件
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// 当在函数中使用生命周期注解时,这些注解只出现在函数签名中,而不存在于函数体中的任何代码中
// 这是因为 Rust 能够分析函数中代码而不需要任何协助
if x.len() > y.len() {
x // 执行 if 时,x 被返回,函数返回的引用 与外部传入参数 x 的引用 保持一致的生命周期
} /* 而 y 依然在此处离开作用域并被清理 */ else {
y // 执行 else 时,y 被返回,函数返回的引用 与外部传入参数 y 的引用 保持一致的生命周期
} // 而 x 依然在此处离开作用域并被清理
}
回顾 “lifetime elision” ,结合上面冗长的注释说明,可以知道类似于 这样 fn longest(x: &str, y: &str) -> &str { }
参数和返回值都是引用的函数签名:
- 根据第一条规则,等价展开成
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { }
- 由于不符合一个输入生命周期参数的条件,不适用于第二条规则
- 由于不存在参数
&self
或&mut self
,不适用于第三条规则
编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期,因此需要手动标注生命周期。
针对函数生命周期参数的补充说明:
- 通过在函数签名中指定生命周期参数时,我们 并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。
- ★
longest
函数并不需要知道x
和y
具体会存在多久,而只需要知道有某个可以被'a
替代的作用域将会满足这个签名。 - 当 具体的 引用被传递给
longest
时,被'a
所替代的具体生命周期是x
的作用域与y
的作用域相重叠的那一部分。
换一种说法就是,泛型生命周期'a
的具体生命周期等同于x
和y
的生命周期中较小的那一个。
因为我们用相同的生命周期参数'a
标注了返回的引用值,所以返回的引用值就能保证在x
和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
} // 编译器提供的错误信息:- `string2` dropped here while still borrowed
// 错误表明:为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的
// string1 更长,因此 result 最终会包含指向 string1 的引用
// 又因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的
// 然而我们通过生命周期参数告诉 Rust 的是:
// longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。
// 因此,借用检查器认为这下面这句代码 `可能` 会存在无效的引用,所以不能通过编译
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
longest
函数:// 返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。
// 牢记,生命周期的定义是:引用保持有效的作用域
// 在函数中使用生命周期的前提是函数返回引用,返回的不是引用就不需要生命周期注解
fn longest(x: &str, y: &str) -> String { // String 是有所有权的数据类型,不是引用
// 即无论 if 分支是哪个,最终函数都会返回一个 String,而不会像返回引用那样考虑作用域问题(生命周期)
if x.len() > y.len() {
x.to_string()
} else {
y.to_string()
}
}
// 返回不是引用类型的 str 可以吗?答案是不行。
fn longest(x: &str, y: &str) -> str {
// 编译器告诉你:the size for values of type `str` cannot be known at compilation time
// the return type of a function must have a statically known size
if x.len() > y.len() {
*x.clone()
} else {
*y.clone()
}
}
结构体定义中的生命周期注解
当结构体的字段是引用类型时,必须添加生命周期注解。类似于泛型参数类型,在结构体名称后面的尖括号中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。struct ImportantExcerpt<'a> { // 这个注解意味着 `ImportantExcerpt` 的实例不能比其 `part` 字段中的引用存在的更久
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 };
}
方法定义中的生命周期注解
声明和使用生命周期参数的位置依赖于生命周期参数是否同结构体字段或方法参数和返回值相关。
(实现方法时)结构体字段的生命周期必须总是在impl
关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。impl
块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在 方法签名 (注意不是impl
块上)中使用生命周期注解。// 带一个生命周期注解的例子
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> { // impl 之后和类型名称之后的生命周期参数是必要的
// 返回的内容并不是引用,而且只有一个 `&self` 符合第 1、3条规则,无需标注生命周期
fn level(&self) -> i32 {
3
}
// 应用第一条生命周期省略规则并给予 &self 和 announcement 他们各自的生命周期
// 因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
从两个生命周期注解的例子可以更清楚地认识到,提前定义好 <’a, ‘b> 的顺序是很有必要的,因为我们完全在定义结构体生命周期参数// 带两个生命周期注解的例子
#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a, 'b> {
part: &'a str,
info: &'b str, }
impl<'a, 'b> ImportantExcerpt<'a, 'b> {
// Self 不是引用,所以无需给返回值生命周期注解
// 两个参数必须生命周期注解,因为结构体字段是无序的
fn new(p: &'a str, i: &'b str) -> Self {
Self { part: p, info: i }
}
// 交换参数位置也行,只要生命周期注解对应上结构体定义的生命周期注解的顺序即可
fn new_exchange_argpos(i: &'b str, p: &'a str) -> Self {
Self { info: i, part: p }
}
}
}
struc ...<'a, 'b>
的时候使用 a 和 b 的注解名称,而在定义方法生命周期参数impl<'c, 'd> ... <'c, 'd>
的时候使用 c 和 d 的注解名称:// 结构体和方法使用不同的生命周期注解
#![allow(unused)]
fn main() {
struct ImportantExcerpt<'a, 'b> {
part: &'a str,
info: &'b str,
}
// 重要的是 `<...>` 里面的顺序,而不是名称
impl<'c, 'd> ImportantExcerpt<'c, 'd> {
fn new(p: &'c str, i: &'d str) -> Self {
Self { part: p, info: i }
}
fn new_exchange_argpos(i: &'d str, p: &'c str) -> Self {
Self { info: i, part: p }
}
}
let tmp = ImportantExcerpt::new("a", "b");
println!("{:?}", tmp);
// ImportantExcerpt { part: "a", info: "b" }
let temp_ex = ImportantExcerpt::new_exchange_argpos("b", "a");
println!("{:?}", temp_ex);
// ImportantExcerpt { part: "a", info: "b" }
}
结合泛型和 trait 的生命周期
结合泛型类型参数、trait bounds 和生命周期的语法展示:
使用<生命周期标注, 泛型类型参数>
来同时声明。因为生命周期也是泛型,所以生命周期参数'a
和泛型类型参数T
都位于函数名后的同一尖括号列表中。use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display /* T 是任意类型,无需生命周期注解*/
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
静态生命周期
静态生命周期使用'static
标注,其生命周期 能够 存活于整个程序期间。
所有的字符串字面值 (str) 都拥有'static
生命周期,因为它们是被直接储存在程序的二进制文件中而这个文件总是可用的。我们也可以选择像下面这样标注出来:
你可能在错误信息的帮助文本中见过使用let s: &'static str = "I have a static lifetime.";
'static
生命周期的建议,不过将引用指定为'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你也许要考虑是否希望它存在得这么久,即使这是可能的。
大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个'static
的生命周期。