UP | HOME

泛型

Table of Contents

每一个编程语言都有高效处理重复概念的工具。在 Rust 中其工具之一就是 泛型 (generics)。泛型是 具体类型其他属性抽象替代

  泛型可以表达泛型的属性 ,比如他们的行为或如何与其他泛型相关联,而不需要在编写和编译代码时知道他们在这里实际上代表什么

  当然可以编写一份可以用于多种具体值的代码,函数并不知道其参数为何值,这时就可以让函数获取泛型而不是像 i32 或 String 这样的具体值

  前面讨论过的 Option<T>,Vec<T> 和 HashMap<K, V>,以及 Result<T, E> 这些都属于泛型

这里将:

  1. 回顾一下提取函数以减少代码重复的机制
  2. 使用相同的技术,从两个仅参数类型不同的函数中创建一个泛型函数
  3. 结构体和枚举定义中的泛型

之后讨论 trait ,这是一个 定义泛型行为 的方法

trait 可以与泛型结合来将泛型限制为拥有特定行为的类型,而不是任意类型

最后介绍 生命周期 ,它是一类允许向 编译器 提供 引用如何相互关联 的泛型

Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性

提取函数来减少重复

    在介绍泛型语法之前,首先来回顾一个不使用泛型的处理重复的技术:提取一个函数
  
    当熟悉了这个技术以后,将使用相同的机制来提取一个泛型函数!
  
    如同你识别出可以提取到函数中重复代码那样,你也会开始识别出能够使用泛型的重复代码

考虑一下这个寻找列表中最大值的小程序,如下所示:

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);
}
1. 这段代码获取一个整型列表,存放在变量 number_list 中。它将列表的第一项放入了变量 largest 中
2. 接着遍历了列表中的所有数字,如果当前值大于 largest 中储存的值,将 largest 替换为这个值
3. 如果当前值小于或者等于目前为止的最大值,largest 保持不变
4. 当列表中所有值都被考虑到之后,largest 将会是最大值,在这里也就是 100

如果需要在两个不同的列表中寻找最大值,可以重复上面示例种的代码,这样程序中就会存在两段相同逻辑的代码:

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![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
}
  虽然代码能够执行,但是重复的代码是冗余且容易出错的,并且意味着当更新逻辑时需要修改多处地方的代码

为了消除重复,可以创建一层抽象,在这个例子中将表现为一个获取任意整型列表作为参数并对其进行处理的函数

  这将增加代码的简洁性并将表达和推导寻找列表中最大值的这个概念与使用这个概念的特定位置相互独立

将寻找最大值的代码提取到了一个叫做 largest函数 中。这不同于前面的代码只能在一个特定的列表中找到最大的数字,这个程序可以在两个不同的列表中找到最大的数字:

fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

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

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
}
largest 函数有一个参数 list,它代表会传递给函数的任何具体的 i32值的 slice

函数定义中的 list 代表任何 &[i32]。当调用 largest 函数时,其代码实际上运行于传递的特定值上

总的来说,经历了如下几步:

  1. 找出重复代码
  2. 将重复代码提取到了一个函数中,并在函数签名中指定了代码中的输入和返回值
  3. 将重复代码的两个实例,改为调用函数
    如果我们有两个函数,一个寻找一个 i32 值的 slice 中的最大项而另一个寻找 char 值的 slice 中的最大项该怎么办?该如何消除重复呢?

在不同的场景使用不同的方式,可以利用相同的步骤和泛型来减少重复代码。与函数体可以在抽象list而不是特定值上操作的方式相同,泛型允许代码对抽象类型进行操作

泛型

  可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型
  
  看看如何使用泛型定义函数、结构体、枚举和方法,然后讨论泛型如何影响代码性能

在函数定义中使用泛型

当使用泛型定义函数时,在 函数签名 中通常为 参数返回值 指定数据类型的位置放置泛型

   以这种方式编写的代码将更灵活并能向函数调用者提供更多功能,同时不引入重复代码

回到 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);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
}
largest_i32 函数是前面提取的寻找 slice 中 i32 最大值的函数

largest_char 函数寻找 slice 中 char 的最大值

这两个函数有着相同的代码,所以在一个单独的函数中引入泛型参数来消除重复

为了参数化要定义的函数的签名中的类型,需要像给函数的值参数起名那样为 这类型参数 起一个 名字

   任何标识符都可以作为类型参数名
   
   不过选择 T 是因为 Rust 的习惯是让变量名尽量短,通常就只有一个字母
   
   同时 Rust 类型命名规范是骆驼命名法。T 作为 “type” 的缩写是大部分 Rust 程序员的首选
  • 当需要在函数体中使用一个参数时,必须在 函数签名声明 这个 参数 以便编译器能知道函数体中这个名称的意义
  • 同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它

为了定义泛型版本的 largest 函数, 类型参数声明 位于 函数名称参数列表 中间的尖括号 <> 中,像这样:

fn largest<T>(list: &[T]) -> T {
这可以理解为:函数 largest 有泛型类型 T:
1. 它有一个参数 list,它的类型是一个 T 值的 slice
2. largest 函数将会返回一个与 T 相同类型的值

下面展示一个在签名中使用了泛型的统一的 largest 函数定义。该示例也向展示了如何对 i32 值的 slice 或 char 值的 slice 调用 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 的函数体不能适用于 T 的所有可能的类型。因为在函数体需要比较 T 类型的值,不过它只能用于我们知道如何排序的类型

     注释中提到了 std::cmp::PartialOrd,这是一个 trait:可以实现类型的比较功能
   
     在 “trait 作为参数” 部分会讲解如何指定泛型实现特定的 trait

结构体定义中的泛型

同样也可以使用 <> 语法来定义拥有一个或多个 泛型参数类型字段 的结构体。下面展示了如何定义和使用一个可以存放任何类型的 x 和 y 坐标值的结构体 Point:

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

其语法类似于函数定义中使用泛型:

  1. 必须在 结构体名称 后面的尖括号中 声明 泛型参数的名称
  2. 在结构体定义中可以 指定 具体数据类型的位置 使用 泛型类型

注意 Point<T> 的定义中只使用了一个泛型类型,这个定义表明结构体 Point<T> 对于一些类型 T 是泛型的,而且字段 xy 都是 相同类型 的,无论它具体是何类型。如果尝试创建一个有不同类型值的 Point<T> 的实例,像下面的不能编译:

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

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

在这个例子中,当把整型值 5 赋值给 x 时,就告诉了编译器这个 Point<T> 实例中的泛型 T 是整型的。接着指定 y 为 4.0,它被定义为与 x 相同类型,就会得到一个像这样的类型不匹配错误:

error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found
floating-point number
  |
  = note: expected type `{integer}`
             found type `{float}`

如果想要定义一个 x 和 y 可以有不同类型且仍然是泛型的 Point 结构体,可以使用 多个 泛型类型参数。修改 Point 的定义为拥有两个泛型类型 T 和 U。其中字段 x 是 T 类型的,而字段 y 是 U 类型的:

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<T> 枚举:

enum Option<T> {
    Some(T),
    None,
}
   现在这个定义看起来就更容易理解了。Option<T> 是一个拥有泛型 T 的枚举,它有两个成员:Some,它存放了一个类型 T 的值,和不存在任何值的None
   
   通过 Option<T> 枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T> 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象

枚举也可以拥有多个泛型类型。使用过的 Result 枚举定义就是一个这样的例子:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
Result 枚举有两个泛型类型,T 和 E。Result 有两个成员:
1. Ok,它存放一个类型 T 的值
2. Err 则存放一个类型 E 的值

这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作

前一章中打开一个文件的场景:当文件被成功打开 T 被放入了 std::fs::File 类型而当打开文件出现问题时 E 被放入了 std::io::Error 类型

当发现代码中有多个只有存放的值的类型有所不同的结构体或枚举定义时,应该像之前的函数定义中那样引入泛型类型来减少重复代码

方法定义中的泛型

可以为定义中 带有泛型结构体枚举 实现 方法 。下面展示了前面定义的结构体 Point<T>,和在其上实现的名为 x 的方法:

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

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) 之间的距离,并使用了只能用于浮点型的数学运算符

结构体定义 中的泛型类型参数并 不总是结构体方法签名 中使用的泛型是 同一类型 。下面实例中 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)的 p1,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 声明而另一些通过方法定义声明的情况:

  • 这里泛型参数 TU 声明于 impl 之后,因为他们与 结构体定义 相对应
  • 泛型参数 VW 声明于 fn mixup 之后,因为他们只是相对于 方法 本身的

泛型代码的性能

Rust 实现了泛型,使得使用泛型类型参数的代码相比使用具体类型并 没有任何速度上的损失 : 通过在 编译时 进行 泛型代码的 单态化 来保证效率。这是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程

   编译器所做的工作正好与创建泛型函数的步骤相反
   
   编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码

看看一个使用标准库中 Option 枚举的例子:

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 指定泛型是任何拥有特定行为的类型
    注意:trait 类似于其他语言中的常被称为接口的功能,虽然有一些不同

定义 trait

一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义是一种将 方法签名 组合 起来的方法,目的是定义一个 实现 某些目的必需的行为集合

     例如,这里有多个存放了不同类型和属性文本的结构体:
   
     结构体 NewsArticle 用于存放发生于世界各地的新闻故事,而结构体 Tweet 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据
   
     如果想要创建一个多媒体聚合库用来显示可能储存在 NewsArticle 或 Tweet 实例中的数据的总结:每一个结构体都需要的行为是他们是能够被总结的
   
     这样的话就可以调用实例的 summarize 方法来请求总结
   

下面展示了一个表现这个概念的 Summary trait 的定义:

pub trait Summary {
    fn summarize(&self) -> String;
}
  • 这里使用 trait 关键字来 声明 ,后面是 trait 的名字 : 例子中是 Summary
  • 大括号 中声明描述实现这个 trait 的类型所需要的行为的方法签名:在这个例子中是 fn summarize(&self) -> String
  • 在方法签名后跟分号,而不是在大括号中提供其实现
     每一个实现这个 trait 的类型都需要提供其自定义行为的方法体
   
     编译器也会确保任何实现 Summary trait 的类型都拥有与这个签名的定义完全一致的 summarize 方法

trait 体中可以有多个方法:一行一个方法签名且都以分号结尾

为类型实现 trait

现在已经定义了 Summary trait,接着就可以在多媒体聚合库中需要拥有这个行为的类型上 实现 它了。下面示例中展示了:

  • NewsArticle 结构体上 Summary trait 的一个实现,它使用标题、作者和创建的位置作为 summarize 的返回值
  • 对于 Tweet 结构体,选择将 summarize 定义为用户名后跟推文的全部文本作为返回值,并假设推文内容已经被限制为 280 字符以内
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

     例如,不能在 aggregator crate 中为 Vec<T> 实现 Display trait
   
     这是因为 Display 和 Vec<T> 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中

这个限制是被称为 相干性 的程序属性的一部分,或者更具体的说是 孤儿规则 ,其得名于不存在父类型

     这条规则确保了其他人编写的代码不会破坏你代码,反之亦然
   
     没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现

默认实现

可以为 trait 中的某些或全部方法提供 默认的行为 ,而不是在每个类型的每个实现中都定义自己的行为是很有用的

    这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为

下面展示了如何为 Summary trait 的 summarize 方法指定一个默认的字符串值,而不是只是定义方法签名:

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

如果想要对 NewsArticle 实例使用这个默认实现,而不是定义一个自己的实现,则可以通过 impl Summary for NewsArticle {} 指定一个 空的 impl 块

impl Summary for NewsArticle {
}

因为提供了一个默认实现并且指定 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 best
    hockey 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 了,而 summary 的默认实现会调用提供的 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
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
       然而,使用过多的 trait bound 也有缺点
     
       每个泛型有其自己的 trait bound,所以有多个泛型参数的函数在名称和参数列表之间会有很长的 trait bound 信息,这使得函数签名难以阅读

为此,Rust 有另一个在函数签名之后的 where 从句 中指定 trait bound 的语法。可以这么写:

fn some_function<T, U>(t: T, u: U) -> i32
    where 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 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,
        }
    }
}

编译会报如下的错误:

error[E0308]: if and else have incompatible types
  --> src/main.rs:88:9
   |
79 |   /     if switch {
80 |   |         NewsArticle {
   |  _|_________-
81 | | |             headline: String::from("Penguins win the Stanley Cup Championship!"),
82 | | |             location: String::from("Pittsburgh, PA, USA"),
83 | | |             author: String::from("Iceburgh"),
84 | | |             content: String::from("The Pittsburgh Penguins once again are the best
85 | | |             hockey team in the NHL."),
86 | | |         }
   | |_|_________- expected because of this
87 |   |     } else {
88 | / |         Tweet {
89 | | |             username: String::from("horse_ebooks"),
90 | | |             content: String::from("of course, as you probably already know, people"),
91 | | |             reply: false,
92 | | |             retweet: false,
93 | | |         }
   | |_|_________^ expected struct `NewsArticle`, found struct `Tweet`
94 |   |     }
   |   |_____- if and else have incompatible types
   |
   = note: expected type `NewsArticle`
              found type `Tweet`

使用 trait bounds 来修复 largest 函数

     现在知道了如何使用泛型参数 trait bound 来指定所需的行为
   
     该回到前面示例中,来修复使用泛型类型参数的 largest 函数定义!

回顾一下,最后尝试编译代码时出现的错误是:

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 函数体中想要使用大于运算符 > 比较两个 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。正如前面 “只在栈上的数据:拷贝” 部分讨论过的,像 i32 和 char 这样的类型是已知大小的并可以储存在栈上,所以他们实现了 Copy trait
   
     将 largest 函数改成使用泛型后,现在 list 参数的类型就有可能是没有实现 Copy trait 的。这意味着可能不能将 list[0] 的值移动到 largest 变量中,这导致了上面的错误

为了只对实现了 Copy 的类型调用这些代码,可以在 T 的 trait bounds 中增加 Copy:

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 方法
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:

let s = 3.to_string();

总结

trait 和 trait bound 让使用泛型类型参数来减少重复,并仍然能够向编译器明确指定泛型类型需要拥有哪些行为。因为向编译器提供了 trait bound 信息,它就可以检查代码中所用到的具体类型是否提供了正确的行为

在动态类型语言中,如果尝试调用一个类型并没有实现的方法,会在运行时出现错误

Rust 将这些错误移动到了编译时,甚至在代码能够运行之前就强迫修复这样的错误

另外,也无需编写运行时检查行为的代码,因为在编译时就已经检查过了,这样相比其他那些不愿放弃泛型灵活性的语言有更好的性能

生命周期

前面在讨论 引用和借用 部分时,遗漏了一个重要的细节:Rust 中的每一个引用都有其 生命周期 (lifetime) ,也就是 引用保持有效的作用域

大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样

但是类似于当因为有多种可能类型的时候必须注明类型,也可能会出现引用的生命周期以一些不同方式相关联的情况

Rust 需要使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的

生命周期的概念某种程度上说不同于其他语言中类似的工具,毫无疑问这是 Rust 最与众不同的功能

这里只会讲到一些通常可能会遇到的生命周期语法以便熟悉这个概念

生命周期避免了悬垂引用

生命周期的主要目标是 避免悬垂引用 ,它会导致程序引用了非预期引用的数据。下面的程序,它有一个外部作用域和一个内部作用域:

{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}
   注意:这里确实声明了没有初始值的变量,所以这些变量存在于外部作用域。这乍看之下好像和 Rust 不允许存在空值相冲突
   
   然而如果尝试在给它一个值之前使用这个变量,会出现一个编译时错误,这就说明了 Rust 确实不允许空值
  1. 外部作用域声明了一个没有初值的变量 r
  2. 内部作用域声明了一个初值为 5 的变量x:在内部作用域中,尝试将 r 的值设置为一个 x 的引用
  3. 在内部作用域结束后,尝试打印出 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 编译器有一个 借用检查器 ,它比较作用域来确保所有的借用都是有效的。下面展示了带有变量生命周期的注释:

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

这里将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b ,内部的 'b 块要比外部的生命周期 'a 小得多

    在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象
    
    程序被拒绝编译,因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短 

现在修改成没有产生悬垂引用且可以正确编译:

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

这里 x 拥有生命周期 'b,比 'a 要大

    这就意味着 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的时候也总是有效的

接下来看看在 函数的上下文参数返回值泛型生命周期

函数中的泛型生命周期

假设有一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice:

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 和 字符串字面值

上面代码应该会打印出:

The longest string is abcd 

如果尝试像下面那样来实现 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 那样:

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

这个函数签名表明对于某些生命周期 'a:

  • 函数会获取两个参数,他们都是与生命周期 'a 存在的一样长的字符串 slice
  • 函数会返回一个同样也与生命周期 'a 存在的一样长的字符串 slice
  这就是告诉 Rust 需要其保证的契约:

  通过在函数签名中指定生命周期参数时,并没有改变任何传入后返回的值的生命周期,而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝

  注意: longest 函数并不需要知道 x 和 y 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名

运行这段代码后会看到想要的输出:

The longest string is abcd

当在函数中使用生命周期注解时,这些注解出现在函数签名中,而 不存在函数体任何代码 中,因为 Rust 能够分析函数中代码而不需要任何协助,不过当函数引用或被函数之外的代码引用时,让 Rust 自身分析出参数或返回值的生命周期几乎是不可能的,而这些生命周期在每次函数被调用时都可能不同

  当具体的引用被传递给 longest 时,被 'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分

  换一种说法就是泛型生命周期 'a 的具体生命周期等同于 x 和 y 的生命周期中较小的那一个

  因为用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 x 和 y 中较短的那个生命周期结束之前保持有效

现在来看看如何通过传递拥有 不同 具体生命周期的引用来限制 longest 函数的使用。下面是一个很直观的例子:

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 和 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:22:44
   |
22 |         result = longest(string1.as_str(), string2.as_str());
   |                                            ^^^^^^^ borrowed value does not live long enough
23 |     }
   |     - `string2` dropped here while still borrowed
24 |     println!("The longest string is {}", result);
   |                                          ------ borrow later used here

错误表明为了保证 println! 中的 result 是有效的, string2 需要直到 外部作用域结束 都是 有效的 。Rust 知道这些是因为 longest 函数的参数和返回值都使用了 相同的生命周期参数 'a

  如果从人的角度读上述代码,可能会觉得这个代码是正确的:string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的

  然而,通过生命周期参数告诉 Rust 的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许这样的代码,因为它可能会存在无效的引用

  这个例子揭示了 result 的引用的生命周期必须是两个参数中较短的那个

深入理解生命周期

指定 生命周期参数正确方式 依赖 函数实现的 具体功能 。例如,如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。如下代码将能够编译:

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 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为

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

接下来,将定义包含引用的结构体,不过这需要为结构体定义中的 每一个引用 添加 生命周期注解 。下面示例中有一个存放了一个字符串 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 实例中的引用是有效的

生命周期省略

   现在已经知道了每一个引用都有一个生命周期,而且需要为那些使用了引用的函数或结构体指定生命周期

然而,前面几章中却有一个实例,它没有生命周期注解却能编译成功:

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[..]
}

在早期版本的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:

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

在编写了很多 Rust 代码后,Rust 团队发现在特定情况下程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解

   这里提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的
   
   未来只会需要更少的生命周期注解

被编码进 Rust 引用分析的模式被称为 生命周期省略规则

   这并不是需要程序员遵守的规则;这些规则是一系列特定的场景,此时编译器会考虑,如果代码符合这些场景,就无需明确指定生命周期
   
   省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,它不会猜测剩余引用的生命周期应该是什么。在这种情况,编译器会给出一个错误,这可以通过增加对应引用之间相联系的生命周期注解来解决
   

函数方法的参数 的生命周期被称为 输入生命周期 ,而 返回值 的生命周期被称为 输出生命周期 。编译器采用三条规则来判断引用何时不需要明确的注解:

  • 第一条规则:每一个 引用的参数 都有它 自己的 生命周期参数
  换句话说就是,有一个引用参数的函数有一个生命周期参数

  例如: 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 ,那么 self 的生命周期赋给所有 输出生命周期参数
第三条规则使得方法更容易读写,因为只需更少的符号

如果编译器 检查 完这三条规则后仍然 存在 没有计算生命周期的引用 ,编译器将会 停止并生成错误 。这些规则适用于 fn 定义,以及 impl 块

规则应用

假设我们自己就是编译器。并应用这些规则来计算示例上面的 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 参数

    应用了三个规则之后编译器还没有计算出返回值类型的生命周期
    
    这就是为什么在编译代码时会出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期

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

当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法:

  • 声明使用 生命周期参数的位置 依赖生命周期参数 是否同 结构体字段方法参数返回值 相关
  • 实现方法时: 结构体字段生命周期 必须总是在 impl 关键字之后 声明 并在 结构体名称 之后 被使用 ,因为这些生命周期是结构体类型的一部分
  • impl 块 里的 方法签名 中, 引用 可能与 结构体字段 中的引用 相关联 ,也可能是 独立的
生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解

现在看看一些使用示例中定义的结构体 ImportantExcerpt 的例子,首先,这里有一个方法 level。其唯一的参数是 self 的引用,而且返回值只是一个 i32,并不引用任何值:

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

impl 之后和类型名称之后的 生命周期参数必要的

因为第一条生命周期规则并不需要标注 self 引用的生命周期 

这里是一个适用于第三条生命周期省略规则的例子:

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 生命周期,可以选择像下面这样标注出来:

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

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的

   你可能在错误信息的帮助文本中见过使用 'static 生命周期的建议,不过将引用指定为 'static 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效
   
   你可能会考虑希望它一直有效,不过大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static 的生命周期

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

在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法:

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

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

返回两个字符串 slice 中较长者的 longest 函数,增加了一个额外的参数 ann

ann 的类型是泛型 T,它可以被放入任何实现了 where 从句中指定的 Display trait 的类型

这个额外的参数会在函数比较字符串 slice 的长度之前被打印出来,这也就是为什么 Display trait bound 是必须的

总结

现在知道了 泛型类型参数traittrait bounds 以及 泛型生命周期类型 ,这样能帮助你编写既不重复又能适用于多种场景的代码了:

  • 泛型类型参数意味着代码可以适用于不同的类型
  • trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为
  • 生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用

而所有的这一切发生在编译时所以不会影响运行时效率!

这个领域还有更多需要学习的内容:
1. 以后会讨论 trait 对象,这是另一种使用 trait 的方式
2. 还会涉及到生命周期注解更复杂的场景
3. 最后还要讲解一些高级的类型系统功能

不过接下来,先看下如何在 Rust 中编写测试,来确保代码的所有功能能像希望的那样工作!

Next:测试

Previous:错误处理

Home: 目录