0%

Rust 学习笔记:第10章 泛型、trait、生命周期

消除重复代码

重复代码

  • 重复代码危害
    • 容易出错
    • 需求变更时需要在多处进行修改
  • 消除重复:提取函数

消除重复的步骤

  • 识别重复代码
  • 提取重复代码到函数体中,并在函数签名中指定函数的输入和返回值
  • 将重复的代码使用函数调用进行替代
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {}", largest);

let number_list = vec![34, 50, 25, 100, 65, 60, 70];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}

println!("The largest number is {}", largest);
}

对于上述的重复代码,可以将一些操作抽到函数中,具体代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for number in list {
if number > largest {
largest = number;
}
}
largest
}

fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list)
println!("The largest number is {}", result);

let number_list = vec![34, 50, 25, 100, 65, 60, 70];
let result = largest(&number_list)
println!("The largest number is {}", result);
}

泛型

  • 泛型:提高代码复用能力
    • 处理重复代码的问题
  • 泛型是具体类型或其它属性的抽象代替
    • 你编写的代码不是最终的代码,而是一种模版,里面有一些“占位符”
    • 编译器在编译时将“占位符”替换为具体的类型
  • 例如:fn largest<T>(list: &[T]) -> T {...}
  • 泛型类型参数
    • 很短,通常一个字母;通常使用 T 作为泛型参数名称
    • CamelCase(驼峰命名)
    • T:Type 的缩写

函数定义中的泛型

  • 泛型参数
    • 参数类型
    • 返回类型
1
2
3
4
5
6
7
8
9
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item.le(&largest) {
largest = item;
}
}
largest
}

Struct 定义中的泛型

1
2
3
4
5
6
7
8
9
struct Point<T> {
x: T,
y: T,
}

fn main() {
let interger = Point { x: 5, y: 10 };
let float = Point { x : 1.0, y: 4.0 };
}
  • 可以使用多个泛型的类型参数

    • 太多类型参数:你的代码需要重组为多个更小的单元
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct Point<T, U> {
    x: T,
    y: U,
    }

    fn main() {
    let interger = Point { x: 5, y: 1.0 };
    let float = Point { x : 1.0, y: 40 };
    }

    Enum 定义中的泛型

  • 可以让枚举的变体持有泛型数据类型

    • 例如:Option<T>Result<T, E>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum Option<T> {
    Some(T),
    None,
    }

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

    fn main() {}

    方法定义中的泛型

  • 为 struct 或 enum 实现方法的时候,可在定义中使用泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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());
    }
  • 注意:

    • 把 T 放在 impl 关键字后,表示在类型 T 上实现方法
      • 例如:impl<T> Point<T>
    • 只针对具体类型实现方法(其余类型没实现方法)
      • 例如:impl Point<i32>
  • struct 里的泛型参数可以和方法的泛型参数不同

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    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: 4 };
    let p2 = Point { x: "hello", y: 'c' };
    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
    }

泛型代码的性能

  • 使用泛型的代码和使用具体类型的代码运行速度是一样的
  • 单态化(monomorphization
    • 在编译时将泛型替换为具体类型的过程

Trait

  • Trait 告诉 Rust 编译器:
    • 某种类型具有那些并且可以与其他类型共享的功能
  • Trait:抽象的定义共享行为
  • Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
  • Trait 与其它语言的接口(interface)类似,但有些区别

定义一个 Trait

  • Trait 定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为
    • 关键字:trait
    • 只有方法签名,没有具体实现
    • trait 可以有多个方法:每个方法签名占一行,以 ; 结尾
    • 实现该 trait 的类型必须提供具体的方法实现
1
2
3
4
5
pub trait Summary {
fn summarize(&self) -> String;
}

fn main() {}

在类型上实现 Trait

  • 与为类型实现方法类似
  • 不同之处
    • impl Xxxx for Tweet {...}
    • 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
28
29
pub trait Summary {
fn summarize(&self) -> String;
}

pub struct NewArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String
}

impl Summary for NewArticle {
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);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
use demo::Summary;
use demo::Tweet;

fn main() {
let tweet = Tweet{
usernmae: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
};

println!("1 new tweet: {}", yweet.summarize());
}

实现 Trait 的约束

  • 可以在某个类型上实现某个 trait 的前提条件是
    • 这个类型或这个 trait 是在本地 crate 里定义的
  • 无法为外部类型来实现外部的 trait
    • 这个限制是程序属性的一部分(也就是一致性
    • 更具体地说是孤儿规则:之所以这样命名是因为父类型不存在
    • 此归杂确保其他人的代码不能破坏你的代码,反之亦然
    • 如果没有这个规则,两个 crate 可以为同一个类型实现同一个 trait,Rust 就不知道应该使用那个实现了

默认实现

  • 默认实现的方法可以调用 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
28
29
30
pub trait Summary {
// 默认实现
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}

pub struct NewArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String
}

// 直接继承 Summary 默认实现
impl Summary for NewArticle { }

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 作为参数

  • impl Trait 语法:适用于简单情况

    1
    2
    3
    4
    // 使用 impl Summary 来声明 trait 作为参数
    pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
    }
  • Trait bound 语法:可用于复杂情况

    • impl Trait 语法是 Trait bound 的语法糖
    1
    2
    3
    4
    // 使用泛型来声明 trait 作为参数
    pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
    }
  • 使用 + 指定多个 Trait bound:代码不太美观

    1
    2
    3
    4
    // 要求 item 实现了 Summary 与 Display Trait
    pub fn notify(item: impl Summary + Display) {
    println!("Breaking news! {}", item.summarize());
    }
    1
    2
    3
    pub fn notify<T: Summary + Display>(item: T) {
    println!("Breaking news! {}", item.summarize());
    }
  • Trait bound 使用 where 子句

    • 在方法签名后指定 where 子句
    1
    2
    3
    pub fn notify<T: Summary + Display, U: Clone + Debug>(a: T, b: U) -> String {
    format!("Breaking news! {}", a.summarize());
    }

    在 notify 函数签名中有一串很长的 trait 签名约束,这就会导致看起来很复杂并且也不美观,这时候我们可以使用 where 子句来简化 trait 约束

    1
    2
    3
    4
    5
    6
    7
    pub fn notify<T, U>(a: T, b: U) -> String 
    where
    T: Summary + Display,
    U: Clone + Debug
    {
    format!("Breaking news! {}", a.summarize());
    }

    Trait 作为返回类型

  • impl Trait 语法

    • 注意:impl Trait 只能返回确定的同一种类型,返回可能不同类型的代码会报错
1
2
3
4
5
6
7
8
pub fn notify(s: &str) -> impl Summary {
NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
content: String::from("The Pittsburgh once again are the best hockey team in the NHL."),
author: String::from("Iceburgh"),
location: String::from("Pittsburgh, PA, USA"),
}
}

使用 Trait Bound 有条件的实现方法

  • 在使用泛型参数的 impl 块上使用 Trait bound,我们可以有条件的为实现特定 Trait 的类型来实现方法

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

    impl<T> Pair<T> {
    fn new(x: T, y: T) -> self {
    self( x, y )
    }
    }

    // Pair 泛型中是 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);
    }
    }
    }
  • 也可以为实现了其它 Trait 的任意类型有条件的实现某个 Trait

  • 为满足 Trait Bound 的所有类型上实现 Trait 叫做覆盖实现(blanket implementations)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #[stable(feature = "rust1", since = "1.0.0")]
    impl<T: fmt::Display + ?Sized> ToString for T {
    // A common guideline is to not inline generic functions. However,
    // removing `#[inline]` from this method causes non-negligible regressions.
    // See <https://github.com/rust-lang/rust/pull/74852>, the last attempt
    // to try to remove it.
    #[inline]
    default fn to_string(&self) -> String {
    use fmt::Write;
    let mut buf = String::new();
    buf.write_fmt(format_args!("{}", self))
    .expect("a Display implementation returned an error unexpectedly");
    buf
    }
    }

生命周期

  • Rust 的每个引用都有自己的生命周期
  • 生命周期:引用保持有效的作用域
  • 大多数情况:生命周期是隐式的、可被推断的
  • 当引用的生命周期可能以不同的方式互相关联时:需要手动标注生命周期

避免悬垂引用(dangling reference)

  • 生命周期的主要目标:避免悬垂引用(dangling reference)

在 main 函数中有两个作用域,在第三行我们声明了一个r并未赋值(rust必须给变量赋值,不然运行不了),之后我们在进入了一个作用域,这里面声明了 x,然后将 x 引用赋值给了 r,出了作用域之后这次 x 就被回收了,这里还会出现一个错误就是我们借用了被销毁的变量,编译器会提示我们不能这样做。

1
2
3
4
5
6
7
8
9
10
fn main() {
{
let r;
{
let x = 5;
r = &x; //出了变量域之后,x就被销毁了,同时x的引用也不可用了
}
println!("r: {}", r);
}
}

借用检查器

  • Rust 编译器的借用检查器:比较作用域来判断所有借用是否合法
1
2
3
4
5
fn main() {
let x = 5;
let r = &x;
println!("r: {}", r);
}

函数中的泛型生命周期

以下代码会报错,这个函数的返回类型包含一个借用的值,但是函数的签名没有说明这个借用的值是来自x还是来自y,可以考虑引入一个命名的生命周期参数

–> src/main.rs:31:33
|
31 | fn longest(x: &str, y: &str) -> &str {
| —- —- ^ expected named 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
help: consider introducing a named lifetime parameter
|
31 | fn longest<’a>(x: &’a str, y: &’a str) -> &’a str {
| ^^^^ ^^^^^^^ ^^^^^^^ ^^^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

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

之后我们根据提示,来修改一个函数的签名,此时会出现一个问题就是编译器无法确定 longest 函数返回的是x还是y,如果我们将 ‘a 修饰某个函数参数,那么代码依旧会报原先的错误(无论这里返回的是x还是y),此时需要使用 ‘a 来修饰两个变量参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest(string1.as_str(), string2);
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
}
}

生命周期标注-语法

  • 生命周期的标注不会改变引用的生命周期长度
  • 当指定了泛型生命周期参数,函数可以接收带有任何生命周期的引用
  • 生命周期的标注:描述了多个引用的生命周期间的关系,但不影响生命周期
  • 生命周期参数名
    • 以 ‘ 开头
    • 通常全小写且非常短
    • 很多人使用 ‘a
  • 生命周期标注的位置
    • 在引用的 & 符号后
    • 使用空格将标注和引用类型分开

生命周期标注-例子

  • &i32:一个引用
  • &'a i32:带有显式生命周期的引用
  • &'a mut i32:带有显式生命周期的可变引用
  • 单个生命周期标注本身没有意义

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

  • 泛型生命周期参数声明在:函数名和参数列表之间的 <> 里
  • 生命周期 ‘a 的实际生命周期是:x和y两个生命周期中较小的那个
1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

深入理解生命周期

  • 指定生命周期参数的方式依赖于函数所做的事情

    下面例子,通过 longest 函数我们只返回 x,所以就不需要 y 的生命周期了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
    }

    fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
    }
  • 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配

  • 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值

    • 这就是悬垂引用:该值在函数结束就走出了作用域

    下面例子返回了一个悬垂引用,编译时会报错,不能返回一个悬垂引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
    }


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

    如果想要在函数内部创建值,那么只需要返回对象的本身就行了,也就是说我们不需要返回引用,只需要返回对象的所有权,返回之后这对象内存也就由函数的调用者来进行清理了

    1
    2
    3
    4
    fn longest<'a>(x: &'a str, y: &str) -> String {
    let result = String::from("abc");
    result
    }

    Struct 定义中的生命周期标注

  • Struct 里可包括

    • 自持有的类型
    • 引用:需要在每个引用上添加生命周期标注

ImportExcerpt 需要比 part 字段生命周期长,这样 part 才可以持有更长时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ImportExcerpt<'a> {
part: &'a str,
}

fn main() {
let novel = String::from("Call mew Ishmael, Some years ago...");

// 得到 &str 引用
let first_sentence = novel.split('.')
.next()
.expect("Could not found a '.'");

let i = ImportExcerpt {
part: first_sentence
};
}

生命周期的省略

  • 每个引用都有生命周期
  • 需要为使用生命周期的函数或 struct 指定生命周期参数

这段代码在早期的 rust 的编译器中是无法编译通过的,因为我们返回的是一个引用,我们需要显式的在函数签名上标注生命周期,但是 rust 开发团队发现很多开发者在开发中,大量重复去标注生命周期,这使得 rust 开发团队就将一些标注的规则直接写入到编译器中,面对有的场景,开发者就不需要标注生命周期,在编译期间编译器就帮你做了

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

fn main() {}
1
2
3
4
5
6
7
8
9
10
11
12
13
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

fn main() {}

生命周期省略规则

  • 在 Rust 引用分析中所编入的模式称为生命周期省略规则
    • 这些规则无需开发者来遵守
    • 它们是一些特殊情况,由编译器来考虑
    • 如果你的代码符合这些情况,那么就无需显式标注生命周期
  • 生命周期省略规则不会提供完整的推断
    • 如果应用规则后,引用的生命周期仍然模糊不清则会编译错误
    • 解决办法:添加生命周期标注,表明引用间的相互关系

输入、输出生命周期

  • 函数/方法的参数:输入生命周期
  • 函数/方法的返回值:输出生命周期

生命周期省略的三个规则

  • 编译器使用 3 个规则在没有显式标注生命周期的情况下,来确定的引用的生命周期
    • 规则1应用于输入生命周期
    • 规则2、3应用于输出生命周期
    • 如果编译器应用完3个规则之后,仍然有无法确定生命周期的引用则会报错
    • 这些规则适用于 fn 定义和 impl 块
  • 规则1:每个引用类型的参数都有自己的生命周期
  • 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有输出生命周期参数
  • 规则3:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self(是方法),那么 self 的生命周期会被赋给所有的输出生命参数

生命周期省略的三个规则-例子

假设我们是编译器,编译器在编译期间会对生命周期做什么

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

    • fn first_word<'a>(s: &'a str) -> &str {:规则1
    • fn first_word<'a>(s: &‘a str) -> &’a str {:规则2
  • fn longest(x: &str, y: &str) -> &str {

    • fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {:规则1(每个函数参数都有自己的生命周期)

    在应用规则1之后,编译器为x与y添加了生命周期,因为函数参数有两个生命周期所有第2条与第3条生命周期也不适用了,那么此时编译器就会报错,面对这种情况我们就需要显示的去生命周期了。 fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {,需要在函数返回值签名上声明返回的生命周期

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

  • 在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样
  • 在那声明和使用生命周期参数,依赖于:
    • 生命周期参数是否和字段、方法的参数或返回值有关
  • struct 字段的生命周期名
    • 在 impl 后声明
    • 在 struct 名后使用
    • 这些生命周期是 struct 类型的一部分
  • impl 块内的方法签名中:
    • 引用必须绑定于 struct 字段引用的生命周期,或者引用是独立的也可以
    • 生命周期省略规则经常使得方法中的生命周期标注不是必须的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct ImportExcerpt<'a> {
part: &'a str,
}

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

// 会应用第3条规则
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

静态生命周期

  • ‘static 是一个特殊的生命周期:整个程序的持续时间
    • 例如:所有的字符串字面值都拥有 ‘static 生命周期
      • let s: &'static str = "I have a static lifetime";
  • 为引用指定 ‘static 生命周期前要三思
    • 是否需要引用在程序整个生命周期内都存活

泛型参数类型、Trait Bound、生命周期

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

fn main() {}