10泛型、trait与生命周期

重复代码的危害:

  • 容易出错;
  • 需求变更时需要在多出进行修改;

消除重复代码的方式:

方式一:提取函数

  1. 识别重复代码;
  2. 将重复代码提取到一个函数中,并在函数签名中指定了代码中的输入和返回值;
  3. 将重复代码改为调用函数;

方式二:泛型

泛型数据类型

高效处理重复概念的工具。

泛型:提高代码复用能力

  • 处理重复代码问题

泛型是具体类型或属性的抽象代替

  • 你编写的代码不是最终的代码,而是一种模板,里面有一些占位符
  • 编译器在编译时,将占位符替换为具体的类型;(单太化,所以没有运行时开销,效率与不使用泛型一样高)

使用泛型定义函数、结构体、枚举和方法

在函数中使用泛型

使用泛型定义函数时,函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。

当在函数签名中使用一个泛型参数时,必须在使用它之前就声明它(给它起个名字,就像给函数形参起名字一样)。

  1. 先声明泛型参数名称;
  2. 在形参类型和函数返回值类型中使用声明的泛型参数;
  • 泛型参数声明位于函数名称与参数列表中间的尖括号<> 中,

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

  • 任何标识符都可以作为泛型参数的名字,通常使用单个字母,例如 T(type 的缩写),V (value的缩写),E…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 普通函数
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];

for &item in list {
if item > largest {
largest = item;
}
}
largest
}

// 使用泛型定义的函数 (注意这个代码不能编译,缺少内容)
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest = item;
}
}

largest
}


// 调用方式
let vec1 = vec![1,2,3];
// 显示指定参数类型
largest::<i32>(&vec1);
// 隐式指定/推断
largest(&vec1);

结构体定义中的泛型

与在函数定义中使用泛型类似,

  1. 必须在结构体名称后尖括号<>声明泛型参数的名称;
    • 声明时,根据需要声明泛型参数的个数,可以为多个的;
  2. 在结构体中需要指定字段类型的位置,使用声明的泛型类型;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 使用一个类型参数
struct Point<T> {
x: T, // 使用时 x和y 只能是一种类型
y: T,
}
// 使用2个类型参数,由于是泛型,所以这两个参数使用时可以相同,可以不同
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 };
}

枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。

定义泛型参数的方式与结构体一样。

1
2
3
4
5
6
7
8
9
10
11
// 这样就可以表达一个可能存在的值的抽象概念
enum Option<T> {
Some(T), // 存放值
None, // 不存放值
}

// 方便的表达任何可能成功(返回T),也可能失败(返回E)的操作
enum Result<T, E> {
Ok(T),
Err(E),
}

在方法定义中的泛型

因为结构体方法与结构体是分开定义的,所以在方法中使用泛型更灵活一些。

在定义方法时声明和使用泛型类型,有两处要声明泛型类型

  1. impl 后面使用尖括号<> 声明泛型;
  2. 结构体名称后面使用尖括号<>声明泛型;

注意,必须在 impl 后面声明泛型。

必须先声明再使用,否则编译器会报错,提示使用了没有声明的生命周期参数。

原因:在 Rust 中,当结构体使用了泛型,那么为结构体创建方法时,可以为指定类型创建方法,也可以为泛型(例如,T)创建方法。参考 [使用 trait bound 有条件地实现方法](# 使用 trait bound 有条件地实现方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Point<T> {
x: T,
y: T,
}

// 为泛型 T,即所有类型创建x方法,
// 所以 impl 后需要声明T
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
// 为 f32 类型创建distance_from_origin方法,如果类型不是 f32,则无法使用该方法
// 为具体类型创建方法,所以 impl后无需声明T
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

注意点2:

结构体方法签名中使用的泛型可以与结构体中的泛型类型参数不同。

  • 此时,方法中使用的泛型就需要向函数一样在方法名后使用尖括号<>声明;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct Point<T, U> {
x: T,
y: U,
}

// 这里 <T,U> 声明于 impl后,因为与结构体定义相对应,
// <V,W> 声明于方法名 mixup之后,因为它们只与方法本身对应。
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);
}

trait: 定义共享的行为

trait 的功能类似于 Java 中的 接口(interface),scala 中也有 trait。

一个类型的行为由其可供调用的方法构成。

定义 trait

trait 定义,是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必须的行为的集合。

1
2
3
4
5
6
7
[修饰符] trait <trait名称> {
方法签名; // 只有签名,没有实现
方法签名2; // 只有签名,没有实现
默认方法1; // trait 中也可以提供有默认实现的方法;
默认方法2; // 默认方法中可以调用没有实现的方法签名。
...
}

在 trait 中为某些方法提供默认行为,当为某个特定类型实现 trait 时,可以选择保留或重载方法的默认行为。

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

为类型实现 trait

1
2
3
impl <trait名称> for <结构体名> {
[实现trait中的方法] // 如果只有默认方法,就可以是一个空花括号;
}

struct 上实现 trait 方法与实现普通方法的区别:

  • 在于 impl 关键字之后,我们要提供需要实现的 trait 的名字,接着是 for 和需要实现 trait 的类型的名字。

孤儿原则

实现 trait 时的限制:孤儿原则(orphan rule),只有当 trait 或者要实现 trait 的类型两者至少有一个位于 crate 的本地作用域时,才能为该类型实现 trait。不能为外部类型实现外部 trait。

  • 这条规则确保了其他人的代码不会破坏你的代码,反之亦然。

trait 作为参数

如何使用 trait 来接受多种不同类型的参数。

  • 限制参数可接受的类型
    • 不符合要求的不能通过编译
1
2
3
4
// item 参数的类型需要实现了 Summary trait
pub fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}

作为参数时,可以有两种用法:

  • 参数名:impl <trait名称> + <trait名称2>[+...]

    • 使用这种方式而不是具体的类型,表示该参数支持任何实现了指定 trait 的类型。
  • trait Bound 语法

    • 将参数类型限制,写在函数名/方法名后的尖括号<>中,
    • 形如:<T: trait名 + trait名2[+ ...]> ,trait 限制可以有多个。
    1
    2
    3
    pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
    }

impl Trait 与 trait bound 的对比:

  • impl Trait 形式很方便,适用于短小的例子;
  • trait bound 则适用于更复杂的场景。比如多个参数限制时,impl Trait 形式会使得形参列表很长。

注:impl Trait 是 trait bound 形式的语法糖。

即简单场景下的简写。

1
2
3
4
5
6
// impl Trait 形式
pub fn notify(item1: impl Summary + Display, item2: impl Summary + Display) {

// trait bound 形式
pub fn notify<T: Summary + Display>(item1: T, item2: T) {

对比上面两种形式,当更复杂的场景时,trait bound 就更适合一些。

通过 where 简化 trait bound

当使用过多的 trait bound 时多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读。

使用 where 从句 简化 trait bound。

将函数名/方法名后的 trait bound 内容,改写到 where 从句中:

  • where从句写在函数签名后
1
2
3
4
5
6
7
8
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 的类型。

  • 这在闭包和迭代器场景十分有用,第十三章会介绍。

限制:

只能返回同一种类型。

1
2
3
4
5
6
7
8
9
fn returns_summarizable(switch: bool) -> impl Summary {
// 如果多种类型实现了 Summary trait,那么只能允许一种类型返回,不允许返回多种类型。
// 这样是不允许的。
if switch {
NewsArticle {}
} else {
Tweet {}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T{
// 这里需要注意,list[0] 会发生所有权转移,但是因为参数list 是切片,没有其中值的所有权,所以会报错。
// 1. 让slice中的值实现copy trait,这样就不会发生所有权转移;而基本数据类型都实现了copy trait
// 2. 也可以限制其实现 clone
// 3. 或者使用引用
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest = item;
}
}

largest
}

impl Trait 语法,也可以用于返回闭包

在 Rust 中,每个闭包都有唯一的匿名类型,直接作为返回值类是不行的,因为没有具体类型名,这时就可以利用impl Trait的形式来处理。

例如:

1
2
3
4
5
6
7
8
9
10
11
// Returns a function that adds `y` to its input
fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 {
let closure = move |x: i32| { x + y };
closure
}

fn main() {
let plus_one = make_adder_function(1);
assert_eq!(plus_one(2), 3);
}

返回带有闭包的迭代器

可以使用impl Trait 语法返回一个使用 mapfilter 的迭代器。

这样使用mapfilter 会更容易些。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a {
numbers
.iter()
.filter(|x| x > &&0)
// 通过查看 map 的方法签名可知,map返回闭包
.map(|x| x * 2)
}

fn main() {
let singles = vec![-3, -2, 2, 3];
let doubles = double_positives(&singles);
assert_eq!(doubles.collect::<Vec<i32>>(), vec![4, 6]);
}

当 trait 中有关联类型时

注意:当 trait 中有关联类型(type=Item)的时候,需要使用 impl trait名<Item=xxx> 的语法来指定返回的关联类型。否则会报错找不到具体类型。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn combine_vecs(
v: Vec<i32>,
u: Vec<i32>,
// 下面的 Iterator 指定了关联类型 i32;
) -> impl Iterator<Item=i32> {
v.into_iter().chain(u.into_iter()).cycle()
}

fn main() {
let v1 = vec![1, 2, 3];
let v2 = vec![4, 5];
let mut v3 = combine_vecs(v1, v2);
assert_eq!(Some(1), v3.next());
assert_eq!(Some(2), v3.next());
}

如果上面样例返回类型这样写 -> impl Iterator {

会在调用处则会报错如下:

1
consider constraining the associated type `<impl Iterator as Iterator>::Item` to `{integer}`: `<Item = {integer}>`
1
2
3
4
5
6
7
Using the 'turbofish' instead of annotating `doubled`:

**let** a = [1, 2, 3];

**let** doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();

assert_eq!(vec![2, 4, 6], doubled);

使用 trait bound 有条件地实现方法

  1. 通过使用带有 trait bound 的泛型参数的 impl块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::fmt::Display;

struct Pair<T> {
x: T,
y: T,
}

// 所有类型都有 new 方法
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {
x,
y,
}
}
}

// 只有实现了 Display 和 PartialOrd 两个 trait 的类型才有 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);
}
}
}
  1. 可以对任何实现了特定 trait 的类型有条件地实现 trait。对任何满足特定 trait bound 的类型实现 trait 被称为 blanket implementations

    1
    2
    3
    4
    5
    // 为任何实现了 Display trait 的类型实现了 ToString 这个trait,
    // 这样任何实现了 Display trait 的类型都可以调用由 ToString 定义的 to_string 方法。
    impl<T: Display> ToString for T {
    // --snip--
    }

[Trait 的高级特性](19高级特性.md# 高级 trait)

请参考上面的链接地址中的 高级特性部分。

生命周期与引用有效性

生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联,保证内存安装的行为。

Rust 中每一个引用都有生命周期(lifetime),即引用保持有效的作用域。

  • 生命周期的使用与类型相似,即部分场景是可以隐含的并可以推断的,部分场景需要显示标注。

泛型生命周期参数

生命周期避免了悬垂引用

当变量尝试使用离开作用域的值的引用时,会发生悬垂引用,这是Rust 不允许的。

Rust 编译器通过使用借用检查器(borrow checker)

  • 它会比较作用域,确保所有的借用都是有效的。
  • 增加泛型生命周期参数来定义引用间的关系,以便借用检查器可以进行分析。

一个有效的引用,需要数据比引用有着更长的生命周期。

生命周期注解语法

生命周期注解并不改变任何引用的生命周期,只是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。

使用生命周期,也需要像泛型类似,声明一个生命周期参数。

例如,报错内容:

1
2
expected named lifetime parameter
// 意思是,需要一个命名的生命周期参数

生命周期参数名称必须以撇号(')开头,其名称同城全是小写。

  • 'a 是大多数人默认使用的名称
  • 生命周期参数注解,在引用符号& 之后,并由一个空格来将引用类型与生命周期注解分隔开。
1
2
3
&i32        // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

注:单个生命周期注解本身没有意义,因为生命周期注解告诉 Rust 多个引用的泛型声明周期参数如何相互联系的。

重要:引用型参数的生命周期,是告诉Rust,生命周期标注的引用,其生命周期必须与该泛型生命周期一样久

例如:

1
2
3
4
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

// 上面的生命周期注解告诉 Rust ,引用 x 与 y ,必须与这泛型生命周期存在的一样久。

函数签名中的生命周期注解

与泛型类型参数类似,

泛型生命周期参数需要声明在函数名和参数列表间的尖括号<>中。

函数并不需要知道涉及参数会存在多久,只需要知道有某个可以被'a替代的作用域将满足签名

多个引用参数时,生命周期与传入参数的生命周期较短那个保持一致。

1
2
3
4
5
6
7
8
9
10
11
// 下面代码就会报错,因为result的生命周期与string2生命周期保持一致
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
// longest() 函数的定义在上面
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}

结构体定义中的生命周期注解

结构体中可以包含引用类型的成员,此时需要为每个引用添加生命周期。

类似于泛型参数类型,必须在结构体名称后面尖括号<>中声明泛型生命周期参数,以便在结构体定义中使用生命周期参数。

1
2
3
struct ImportantExcerpt<'a> {
part: &'a str,
}

生命周期省略(Lifetime Elision)

生命周期省略规则(lifetime elision rules):

  1. 每个引用的参数都有它自己的生命周期参数。
    • 有一个引用参数的函数有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a,'b>(x:&'a i32,y: &'b i32),依此类推。
  2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
    • fn foo<'a>(x: &'a i32) -> &'a i32
  3. 如果方法有多个输入生命周期参数,其中之一为&self&mut self ,那么 self 的生命周期被赋给所有输出生命周期参数。
    • 第三条规则真正能够适用的只有方法签名。(所以方法签名中一般不需要标注生命周期)
  • 适用范围:这些规则适用于fn定义,和impl定义。

第一条规则适用于输入生命周期(函数/方法的参数的生命周期),二三条适用于输出生命周期(返回值的生命周期)。

生命周期省略规则是给编译器考虑的。

编译器采用上面三条规则来判断何时可以省略生命周期标注。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将报错。

  • 此时需要显示地标注生命周期

方法定义中的生命周期注解

实现方法时,结构体字段的生命周期必须总impl 关键字之后声明,并在结构体名称之后被使用

方法中是否声明和使用生命周期参数,取决于方法的引用参数是否与结构体中的引用字段相关联,方法中可以使用,也可以不使用结构体中的引用字段。

1
2
3
4
5
6
7
8
9
struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

静态生命周期

'static ,静态生命周期,其生命周期能够存活于整个程序期间。

  • 所有字符串字面值都拥有 'static 生命周期。

  • 常量和静态变量也都拥有'static生命周期。

  • 也可以显示的指定

    1
    let s: &'static str="i have a static lifetime.";

注意,将引用指定为'static 之前,需要考虑这个引用是否真的在整个程序的生命周期里都有效。

结合泛型类型参数,trait bounds 和生命周期

1
2
3
4
5
6
7
8
9
10
11
12
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
}
}

因为生命周期也是泛型,所以生命周期参数和泛型类型参数都位于函数名后的同一尖括号列表中。

更多内容参考

第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。

第十九章会涉及到生命周期注解更复杂的场景。

第二十章讲解一些高级的类型系统功能。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信