Rust 基础(四)

news2025/1/15 12:47:00

十、泛型、Traits和生命周期

每种编程语言都有有效处理概念重复的工具。在Rust中,一个这样的工具就是泛型:具体类型或其他属性的抽象替身。我们可以表达泛型的行为,或者它们如何与其他泛型相关联,而不知道在编译和运行代码时它们的位置会是什么。

函数可以接受某种泛型类型的形参,而不是i32或String等具体类型的形参,这与函数接受带有未知值的形参以在多个具体值上运行相同代码的方式相同。事实上,我们已经在第6章使用了Option<T>泛型,第8章使用了Vec<T>HashMap<K, V>,第9章使用了Result<T, E>泛型。在本章中,您将探索如何使用泛型定义自己的类型、函数和方法!

首先,我们将回顾如何提取函数以减少代码重复。然后,我们将使用相同的技术从两个仅在形参类型上不同的函数生成一个泛型函数。我们还将解释如何在结构和枚举定义中使用泛型类型。

然后,您将学习如何使用特征trait以通用的方式定义行为。您可以将特征与泛型类型结合起来,以约束泛型类型只接受具有特定行为的类型,而不是只接受任何类型。

最后,我们将讨论生命周期:向编译器提供关于引用之间如何关联的各种泛型。生命周期允许我们向编译器提供关于借用值的足够信息,以便它能够确保引用在更多的情况下是有效的,而不是在没有我们帮助的情况下。

通过提取函数去除重复

泛型允许我们用表示多个类型的占位符替换特定类型,以消除代码重复。那么,在深入研究泛型语法之前,让我们先看看如何通过提取一个用表示多个值的占位符替换特定值的函数,以一种不涉及泛型类型的方式删除重复。然后我们将应用相同的技术来提取泛型函数!通过了解如何识别可以提取到函数中的重复代码,您将开始识别可以使用泛型的重复代码。

我们从清单10-1中的短程序开始,该程序查找列表中最大的数字。
10-1:

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

我们在变量number_list中存储整数列表,并将列表中的第一个数字引用在一个名为最大变量的变量中。然后,我们遍历列表中的所有数字,如果当前数字大于存储在最大的数字,那么在这个变量中替换引用。然而,如果当前的数字小于或等于迄今所见的最大数字,变量就不会改变,代码就会进入列表中的下一个数字。在考虑了列表中的所有数字之后,最大的数字应该指最多的数字,在这个例子中是100。

我们现在的任务是在两个不同的数字列表中找到最大的数字。为此,我们可以选择在清单10-1中重复代码,并在程序中使用相同的逻辑,如清单10-2所示。
10-2

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

尽管该代码工作,重复的代码繁琐,且容易出错。当我们想要改变它的时候,我们也必须记住在多个地方更新代码

为了消除这种重复,我们将通过定义一个在参数中传递的整数列表来创建一个抽象的函数来创建一个抽象。这个解决方案使我们的代码更加清晰,让我们可以抽象地在列表中找到最大的数字。

在示例 10-3 的程序中将寻找最大值的代码提取到了一个叫做 largest 的函数中。然后我们调用函数在清单10-2的两个列表中找到最大的数字。我们也可以在其他i32值的列表中使用这个函数。

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值的任何切片。因此,当我们调用函数时,代码运行于我们传递的特定值上。

综上所述,以下是我们对从清单10-2更改代码到清单10-3的步骤:
1.识别重复的代码。
2.将重复的代码提取到函数的主体中,并在函数签名中指定该代码的输入和返回值。
3.更新重复代码的两个实例来调用函数。

接下来,我们将使用这些相同的步骤来减少代码重复。同样,函数体可以在抽象list 上操作,而不是特定的值,泛型允许代码在抽象类型上操作。

例如,我们有两个函数:一个在i32值的切片中找到最大的项,一个在一个char值中找到最大的项。我们如何消除这种重复?让我们来看看!

10.1 泛型(generics)

我们使用泛型来为函数签名或结构体的项创建定义,然后我们可以使用许多不同的具体数据类型。让我们先来看看如何使用泛型定义函数、结构、枚举和方法。然后我们将讨论generics如何影响代码性能。

10.1.1 在函数中定义泛型

当定义一个使用泛型的函数时,我们将泛型放在函数的签名中,我们通常会指定参数和返回值的数据类型。这样做可以使我们的代码更加灵活,并为调用者提供更多的功能,同时防止代码重复。

继续使用我们largest 函数,清单10-4显示了两个函数,它们都找到了一个部分的最大值。然后我们将它们结合成一个使用泛型的单一函数。
10-4

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

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

    largest
}

fn largest_char(list: &[char]) -> &char {
    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_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');
}

larsig_i32函数是我们在清单10-3中提取的,它在切片中找到了最大的i32largest_char 函数可以在切片中找到最大的char。函数体具有相同的代码,因此,让我们通过在一个函数中引入泛型类型参数来消除重复。

为了在一个新的单个函数中参数化类型,我们需要命名类型参数,就像我们为函数的值参数所做的一样。您可以使用任何标识符作为一个类型参数名。但我们将使用T,因为根据惯例,在Rust中类型参数名称是短的,通常只是一个字母,而Rust的类型命名约定是CamelCase。Short for “type”,T是大多数Rust程序员的默认选择。

当我们在函数的主体中使用参数时,我们必须在签名中声明参数名,这样编译器就知道该名称意味着什么。类似地,当我们在函数签名中使用一个类型参数名时,我们必须在使用参数名称之前声明类型参数名。为了定义一般largest 的函数,类型名声明放在函数的名称和参数列表之间的尖括号内<>,像这样:

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

我们这样读这个定义:函数largest 在某个类型的T 上是通用的.这个函数有一个参数命名为list,它是T 类型的一个切片,largest 函数将返回一个类型T的值的引用。

清单10-5显示了在其签名中使用泛型数据类型的largest 函数定义。该列表还展示了如何用i32char切片调用函数。注意,此代码还不能编译,但我们稍后会在本章中修改它。

fn largest<T>(list: &[T]) -> &T {
    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 char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

如果我们现在编译这个代码,我们会得到这个错误:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

帮助文本提到std::cmp::PartialOrd,这是一种trait,我们将在下一节讨论trait。现在,知道这个错误表明largest 不会为所有可能的 T类型工作。因为我们想要比较T类型的值,我们只能使用可以排序的值。为了使比较,标准库有你可以在类型上实现std::cmp::PartialOrd特性(参见附录C更多的关于这个特性)。通过遵循帮助文本的建议,我们限制对T只适用于实现了PartialOrd 的类型,这个示例将编译,因为标准库在i32char上实现了std::cmp::PartialOrd

10.1.2 在结构体中定义泛型

我们还可以定义structs,使用<>语法在一个或多个字段中使用泛型类型参数。清单10-6定义了一个10-6 struct,以持有任何类型的xy坐标值。

10-6

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

在struct定义中使用generics的语法与在函数定义中使用的语法相似。首先,我们在结构体的名称之后,声明了尖括号内的类型参数的名称。然后我们在struct定义中使用泛型类型,否则我们将指定具体的数据类型。

注意,因为我们只使用了一个通用的类型来定义点<T>,这个定义说点Point<T>struct在某些<T>类型中是通用的,而字段x和y都是相同的类型,不管这种类型是什么。如果我们创建一个Point<T>的实例,它有不同类型的值,如清单10-7中,我们的代码编译不通过。
10-7

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

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

为了定义一个Point 结构体,x和y都是泛型的,但可以有不同的类型,我们可以使用多个泛型类型参数。例如,在清单10-8中,我们改变了Point的定义,在TU的类型中是通用的,在x的类型中是T和y的类型中是U
10-8

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 实例都被允许了!您可以根据您想要的定义在定义中使用许多泛型类型参数,但是使用多个使得您的代码难以读取如果您发现代码中需要大量的泛型类型,则可以表明您的代码需要重组成更小的部分

10.1.3 在Enum中定义泛型

正如我们在结构体中所做的那样,我们可以定义enums以在它们的变体中持有通用数据类型。让我们再看一看标准库提供的Option<T> enum,我们在第六章中使用了它:

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 枚举有两个泛型类型,TEResult 有两个成员:Ok,它存放一个类型 T 的值,而 Err 则存放
一个类型 E 的值。这个定义使得 Result 枚举能很方便的表达任何可能成功(返回 T 类型的值)也可能失败(返回 E 类型的值)的操作。实际上,这就是我们在示例 9-3 用来打开文件的方式:当成功打开文件的时候,T 对应的是 std::fs::File 类型;而当打开文件出现问题时,E 的值则是 std::io::Error类型。
当你意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。

10.1.4 在方法中的定义泛型

在为结构体和枚举实现方法时(像第五章那样),一样也可以用泛型。示例 10-9 中展示了示例 10-6 中定义的结构体 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());
}

注意,我们必须在impl之后声明T,这样我们就可以使用T来指定我们在类型Point<T>上实现方法。通过在impl后宣布T为通用类型,Rust可以识别出尖括号中的类型是通用类型,而不是具体类型。我们可以选择这个泛型参数一个不同的名称,而不是在struct定义中声明的泛型参数,但是使用相同的名称是传统的。在一个impl中编写的方法,它声明泛型类型将在任何类型的实例中定义,不管具体类型最终以通用类型代替。

我们还可以在定义类型的方法时指定对泛型类型的约束。例如,我们可以仅在Point<f32>实例上实现方法,而不是在Point<T>实例上实现。在清单10-10中,我们使用具体的f32类型,这意味着我们不会在impl之后声明任何类型。
10-10

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

这个代码意味着类型Point<f32>将有一个remote ce_from_origin方法;其他T不是f32Point<T>的不会有这样的方法定义。该方法测量了我们点从坐标(0.0,0.0)点的距离,并使用只用于浮点类型的数学运算。

结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。示例 10-11 中为Point 结构体使用了泛型类型 X1Y1,为 mixup 方法签名使用了 X2Y2 来使得示例更加清楚。这个
方法用 selfPoint 类型的 x 值(类型 X1)和参数的 Point 类型的 y 值(类型 Y2)来创建一个新 Point类型的实例:

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        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);
}

本例的目的是演示一些泛型参数以impl声明的情况,一些是用方法定义声明的。在这里,通用参数X1Y1impl之后被声明,因为它们与struct定义有关。在mixup后,一般参数X2Y2被声明,因为它们只与该方法相关。

10.1.5 使用泛型的代码性能

在使用泛型类型参数时,您可能会想知道是否有运行时的成本。好消息是,使用泛型类型不会使程序运行速度比具体类型更慢。

Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。编译器所做的工作正好与示例 10-5 中我们创建泛型函数的步骤相反。编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。让我们看看一个使用标准库中 Option 枚举的例子:

let integer = Some(5);
let float = Some(5.0);

当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T> 的值并发现有两种Option<T>:一个对应 i32 另一个对应 f64。为此,它会将泛型定义 Option<T> 展开为 i32f64的两个定义,接着将泛型定义替换为这两个具体的定义。
代码的单态化版本看起来类似于以下(编译器使用不同的名称,而不是我们在这里使用的插图):

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的泛型在运行时非常高效。

10.2 Trait:定义共同行为

trait 定义了一个特定类型的功能,可以与其他类型共享。我们可以使用trait以抽象的方式定义共享行为。我们可以使用trait bounds来指定一个通用类型可以是任何具有特定行为的类型。

注意:trait 与其他语言中经常称为接口的特性相似,尽管有一些不同之处。

10.2.1 Defining a Trait

类型的行为包括我们可以调用的方法。如果我们可以在所有这些类型上调用相同的方法,则不同类型共享相同的行为。trait定义是一种将签名组合在一起来定义一组实现目标所必需的行为的方法。

例如,这里有多个存放了不同类型和数量文本的结构体:结构体 NewsArticle 用于存放发生于世界各地的新闻故事,而结构体 Tweet 最多只能存放 280 个字符的内容,以及像是否转推或是否是对推友的回复这样的元数据。

我们想要创建一个名为 aggregator 的多媒体聚合库crate 用来显示可能储存在 NewsArticleTweet 实例中的数据的总结。每一个结构体都需要的行为是他们是能够被总结的,这样的话就可以调用实例的summarize 方法来请求总结。示例 10-12 中展示了一个表现这个概念的公有 Summary trait 的定义:

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

这里使用 trait 关键字来声明一个 trait,后面是 trait 的名字,在这个例子中是 Summary。我们也声明 trait 为 pub 以便依赖这个 crate 的 crate 也可以使用这个 trait,正如我们见过的一些示例一样。在大括号中声明描述实现这个 trait 的类型所需要的行为的方法签名,在这个例子中是fn summarize(&self) −> String

**在方法签名之后,我们没有在大括号内提供一个实现,而是使用分号。**实现此特性的每个类型都必须为该方法的主体提供自己的自定义行为。编译器也会确保任何实现 Summary trait 的类型都拥有与这个签名的定义完全一致的 summarize 方法。

一个trait 可以在有多个方法:方法签名被列为一行,每一行以分号结束。

10.2.2 在类型上实现一个trait

现在我们定义了 Summary trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。示例 10-13中展示了 NewsArticle 结构体上 Summary trait 的一个实现,它使用标题、作者和创建的位置作为summarize 的返回值。对于 Tweet 结构体,我们选择将 summarize 定义为用户名后跟推文的全部文本
作为返回值,并假设推文内容已经被限制为 280 字符以内。

10-13

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

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 方法所拥有的行为。

现在库在 NewsArticleTweet 上实现了Summary trait,crate 的用户可以像调用常规方法一样调用NewsArticleTweet 实例的 trait 方法了。唯一的区别是 trait 必须和类型一起引入作用域以便使用额外的 trait 方法。这是一个二进制 crate 如何利用 aggregator 库 crate 的例子:

use aggregator::{Summary, Tweet};

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

This code prints 1 new tweet: horse_ebooks: of course, as you probably already know, people.

其他依赖 aggregator crate 的 crate 也可以将 Summary 引入作用域以便为其自己的类型实现该 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。这是因为 DisplayVec<T> 都定义于标准库中,它们并不位于 aggregator crate 本地作用域中。这个限制是被称为 相干性(coherence)的程序属性的一部分,或者更具体的说是 孤儿规则(orphan rule),其得名于父类型不存在。这条规则确保了其他人编写的代码不会破坏你代码,反之亦然。没有这条规则的话,两个 crate 可以分别对相同类型实现相同的 trait,而 Rust 将无从得知应该使用哪一个实现。

10.2.3 默认实现

有时,对于某些或所有方法的默认行为是有用的,而不是在每一种类型上都需要实现所有方法的实现。然后,当我们在特定类型上实现特性时,我们可以保留或覆盖每个方法的默认行为。

示例 10-14 中展示了如何为 Summary trait 的 summarize 方法指定一个默认的字符串值,而不是像示例 10-12 中那样只是定义方法签名:

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

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

虽然我们不再直接为 NewsArticle 定义 summarize 方法了,但是我们提供了一个默认实现并且指定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());

summarize 创建默认实现并不要求对示例 10-13 中 Tweet 上的 Summary 实现做任何改变。其原因是重写(overriding )一个默认实现的语法与实现没有默认实现的 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 了,而 summarize的默认实现会调用我们提供的 summarize_author 定义。因为实现了 summarize_authorSummary 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());

注意无法从相同方法的重载实现中调用默认方法。

10.2.4 trait 作为参数

知道了如何定义 trait 和在类型上实现这些 trait 之后,我们可以探索一下如何使用 trait 来接受多种不同类型的参数。
例如在示例 10-13 中为 NewsArticleTweet 类型实现了 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。我
们可以传递任何 NewsArticleTweet 的实例来调用 notify。任何用其它如 Stringi32 的类型调用该函数的代码都不能编译,因为它们没有实现 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:DisplaySummary。这可以通过 + 语法实现:

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 {

we can use a where clause, like this:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{

这个函数签名就显得不那么杂乱,函数名、参数列表和返回值类型都离得很近,看起来跟没有那么多 trait bounds 的函数很像。

10.2.5 返回实现了 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,
        }
    }
}

这里尝试返回 NewsArticle 或 Tweet。这不能编译,因为 impl Trait 工作方式的限制。第十七章的 ” 为使用不同类型的值而设计的 trait 对象” 部分会介绍如何编写这样一个函数。

10.2.6 使用traits来有条件地实现方法

通过使用与使用泛型类型参数的impl块绑定的 trait bound,我们可以有条件地为实现指定trait的类型实现方法。例如,清单10-15中的Pair<T>类型总是实现new函数来返回Pair<T>的新实例(回想第5章的“定义方法”部分,Selfimpl块类型的类型别名,在本例中是Pair<T>)。但是在下一个impl块中,如果Pair<T>的内部类型T实现了支持比较的PartialOrd特征和Display特征,则它才实现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特征的任何类型上实现ToString特征。标准库中的impl块看起来类似于以下代码:

impl<T: Display> ToString for T {
    // --snip--
}

因为标准库有这个覆盖实现,所以我们可以在实现Display特性的任何类型上调用ToString特性定义的to_string方法。例如,我们可以像这样将整数转换为它们对应的String值,因为整数实现了Display:

let s = 3.to_string();

traittrait bound使我们能够编写使用泛型类型参数来减少重复的代码,同时也向编译器指定我们希望泛型类型具有特定的行为。然后编译器可以使用特征绑定信息来检查与我们的代码一起使用的所有具体类型是否提供了正确的行为。在动态类型语言中,如果在没有定义方法的类型上调用方法,则会在运行时得到错误。但是Rust将这些错误转移到编译时,所以我们不得不在代码能够运行之前修复这些问题。此外,我们不必编写在运行时检查行为的代码,因为我们已经在编译时检查过了。这样做可以提高性能,而不必放弃泛型的灵活性。

10.3 用生命周期验证引用

生命周期是我们已经使用过的另一种通用形式。生命周期不是确保类型具有我们想要的行为,而是确保引用在我们需要时有效

我们在第4章的“引用和借用”一节中没有讨论的一个细节是Rust中的每个引用都有一个生命周期(lifetime)也就是该引用有效的范围。大多数情况下,生命周期是隐式和推断的,就像大多数情况下,类型是推断的一样。只有当可能有多种类型时,才必须对类型进行注释。以类似的方式,当引用的生存期可以以几种不同的方式关联时,我们必须注释生存期Rust要求我们使用通用的生命周期参数注释关系,以确保在运行时使用的实际引用肯定是有效的。

注释生命周期甚至不是大多数其他编程语言都有的概念,因此这可能会让人感到陌生。尽管我们不会在本章中完整地讨论生命周期,但我们将讨论您可能遇到的生命周期语法的常见方式,以便您能够熟悉这个概念。

10.3.1 用生命周期避免悬空引用(Dangling References)

生命周期的主要目的是防止悬空引用,悬空引用会导致程序引用的数据不是它打算引用的数据。考虑清单10-16中的程序,它有一个外部作用域和一个内部作用域。

fn main() {
    let r;

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

    println!("r: {}", r);
}

注意:清单10-16、10-17和10-23中的示例声明变量时没有给它们一个初始值,因此变量名存在于外部作用域中。乍一看,这似乎与Rust没有空值相冲突。但是,如果我们试图在给变量赋值之前使用它,我们将得到一个编译时错误,这表明Rust确实不允许空值。

外部作用域声明了一个没有初始值的名为r的变量,内部作用域声明了一个名为x的变量,初始值为5。在内部作用域内,我们尝试将r的值设置为x的引用。然后内部作用域结束,我们尝试打印r中的值。这段代码无法编译,因为在我们尝试使用它之前,r所引用的值已经超出了作用域。下面是错误信息:
在这里插入图片描述
变量x“活得不够长”。原因是当内部作用域在第7行结束时,x将超出作用域。但对于外部作用域,r仍然有效;因为它的范围更大,我们说它“寿命更长”。如果Rust允许这段代码工作,r将引用当x超出作用域时释放的内存,我们试图用r做的任何事情都不会正确工作。那么Rust是如何确定这段代码无效的呢?它使用借用检查器(borrow checker)。

10.3.2 借用检查器

Rust编译器有一个借用检查器,它比较范围以确定是否所有的借用都有效。清单10-17显示了与清单10-16相同的代码,但带有显示变量生命周期的注释。

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

在这里,我们用'a 注释了r的生命周期,用'b 注释了x的生命周期。正如您所看到的,内部的'b 块比外部的'a生命周期块要小得多。在编译时,Rust比较两个生命期的大小,发现r的生命期为'a,但它引用的内存的生命期为'b。程序被拒绝,因为'b'a短:引用的对象没有引用的生命周期长。

清单10-18修复了代码,使其没有悬空引用,并在编译时没有任何错误。

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

在这里,x的生命周期为'b ,在本例中大于'a。这意味着r可以引用x,因为Rust知道当x有效时,r中的引用总是有效的。

现在您已经知道了引用的生存期在哪里,以及Rust如何分析生存期以确保引用始终有效,让我们在函数上下文中探索形参和返回值的泛型生命周期。

10.3.3 函数中的泛型生命周期

我们将编写一个函数,返回两个字符串切片中较长的那个。这个函数将接受两个字符串片,并返回一个字符串片。在我们实现了最长的函数之后,清单10-19中的代码应该打印出he longest string is abcd
10-19

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

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

注意,我们希望函数接受字符串切片,这些字符串切片是引用,而不是字符串,因为我们不希望longest 函数获得其形参的所有权。

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

相反,我们得到了以下关于生命周期的错误:
在这里插入图片描述
帮助文本显示,返回类型需要一个通用的生命周期参数,因为Rust无法判断被返回的引用是指向x还是y。实际上,我们也不知道,因为这个函数体中的if块返回一个指向x的引用,而else块返回一个指向y的引用!

当我们定义这个函数时,我们不知道将被传递到这个函数中的具体值,所以我们不知道if 还是else 会执行。我们也不知道将传入的引用的具体生命周期,因此不能像清单10-17和10-18中那样查看作用域,以确定返回的引用是否总是有效。借款检查器也不能确定这一点,因为它不知道x和y的生存期与返回值的生存期之间的关系。要修复此错误,我们将添加定义引用之间关系的通用生命周期参数,以便借用检查器可以执行其分析。

10.3.4 生命周期注释语法

生命周期注释不会改变任何引用的生存时间。相反,它们描述了多个相互引用的生命周期之间的关系,而不影响生命周期。正如函数可以在签名指定泛型类型形参时接受任何类型一样,通过指定泛型生存期形参,函数也可以接受具有任何生存期的引用。

生命周期注释有一种稍微不同寻常的语法:生命周期注释参数的名称必须以撇号(')开头,并且通常都是小写且非常短,就像泛型类型一样。大多数人使用'a作为第一个生命周期注释。我们将生命周期参数注释放在引用的&后面,使用空格将注释与引用的类型分隔开。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/45082.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[C++]C++入门--引用

​ &#x1f941;作者&#xff1a; 华丞臧 &#x1f4d5;​​​​专栏&#xff1a;【C】 博主Gitee 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎>在评论区指出。 推荐一款刷题网站 &#x1f449;LeetCode…

IPv6进阶:IPv6 过渡技术之IPv6 over IPv4 手动隧道

实验拓扑 R1-R3-R2之间的网络为IPv4环境&#xff1b;PC1及PC2处于IPv6孤岛。 实验需求 R1及R2为IPv6/IPv4双栈设备&#xff1b;在R1及R2上部署IPv6 over IPv4手工隧道使得PC1及PC2能够互相访问。 配置及实现 R3的配置如下 [R3] interface GigabitEthernet0/0/0 [R3-Gigabi…

【Java实战】工作中如何规范控制语句

目录 一、前言 二、控制语句规范 1.【强制】使用switch注意事项 2.【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时&#xff0c;必须先进行 null 判断。 3.【强制】在 if / else / for / while / do 语句中必须使用大括号。 4.【强制】三目运算符高…

[附源码]计算机毕业设计springboot本地助农产品销售系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【C++】string详细介绍及模拟实现string类

【C】string详细介绍及模拟实现string类 文章目录【C】string详细介绍及模拟实现string类1.什么是string2.string常用接口介绍2.1string类对象的常见构造2.2string类对象的容量操作2.3string类对象的访问及遍历操作2.4string类对象的修改操作2.5string类非成员函数3.string类的…

移动跨平台开发跨家选型参考建议

从 iPhone 诞生至今&#xff0c;智能手机风靡全球已将近20年&#xff0c;智能手机操作系统 iOS 和 Android 也成为当仁不让的顶流般的存在&#xff0c;而作为其背后的灵魂&#xff0c;移动应用也随着技术的发展已经越来越丰富。如果从技术层面来讲&#xff0c;移动 App 也从最开…

Cloud-computing 实验镜像 chinaskills_cloud_iaas.iso chinaskills_cloud_paas.iso

Cloud-computing 实验镜像 最近因新项目再次进行云计算环境的搭建&#xff0c; 找这两个镜像&#xff08; 找chinaskills_cloud_paas.iso chinaskills_cloud_iaas.iso&#xff09;颇为费劲&#xff0c;用尽九牛二虎之力总算找到了&#xff0c;该大侠还分享了诸多系统镜像和完…

高衍射效率的偏振无关透射光栅的分析与设计

摘要 光栅&#xff0c;特别是具有与波长相当的特征尺寸的光栅&#xff0c;具有偏振相关的光学特性。 这使得设计的具有高衍射效率的光栅难以用于任意偏振。 根据文献[T. Clausnitzer, et al&#xff0c;Proc. SPIE 5252,174-182&#xff08;2003&#xff09;]中报道的概念&…

VMware-AD域控管理

目录 新建AD用户[ 以张三[zhangsan]、李四[lisi]为例 ] 2.用户信息-属性-管理-编辑&#xff1a; 3.将张三设置为AD域控管理员&#xff0c; 在wqd.com域下新建几个部门&#xff08;IT、HR、PRD&#xff09; 对从主机&#xff08;win7&#xff09;进行AD接管 修改win7计算机名称&…

MATLB|电动车智能充电模式及电力高峰需求预测

目录 0 写在前面 1 电动车 1.1 电动车&#xff08;EV&#xff09; 1.2 电动汽车充电 1.3 智能充电和车联网&#xff08;V2G&#xff09; 1.4 V2G 应用 1.5 可再生能源可用性 1.6 基于价格的收费 2 电动车智能充电 2.1 智能充电 2.2 实时电价 2.3 智能充电模式——算…

国产CPU对比

关于国产CPU&#xff1a;龙芯、飞腾、鲲鹏、海光、申威、兆芯 CPU 是计算机系统的核心和大脑 n CPU&#xff0c;即中央处理器是计算机的运算和控制核心&#xff0c;其功能主要是解释计算机指令以及处理计算机软件中的数据. CISC实际上是以增加处理器本身复杂度作为代价&#xf…

论文翻译:多延迟块频域自适应滤波器

Multidelay Block Frequency Domain Adaptive Filter 作者&#xff1a; JIA-SIEN SOO 和 KHEE K. PANG 文章目录Multidelay Block Frequency Domain Adaptive Filter1.介绍2.MDF自适应滤波器3.仿真结果和性能分析4.计算的复杂性5.结论摘要-本文提出了一种灵活的多延迟块频域自…

农村城镇面板数据集:地级市人均消费与支出2012-2019各省农村数据2013-2019

1、2002-2019年地级市人均消费与支出数据 1、数据来源&#xff1a;wind 2、时间跨度&#xff1a;2012-2019 3、区域范围&#xff1a;287个地级市 4、指标说明&#xff1a; 包含以下四个指标&#xff1a;人均可支配收入&#xff08;农村&#xff09;、人均可支配收入&#…

在el-table表头上引入组件不能实时传参bug

文章目录场景还原解决方法出现原因场景还原 产品要求&#xff1a;点击表格的表头&#xff0c;能触发一个下拉的列表&#xff0c;列表能携带表格的筛选条件&#xff0c;获取相应的数据 写了一个demo&#xff0c;来还原一下bug出现的场景&#xff1a; <div id"demo&qu…

Day15--加入购物车-初始化vuex

1.加入购物车&#xff1a; 我的操作&#xff1a; ************************************************************************************************************* 2.购物车里面的商品数据在多个页面都会用到。所以把购物车里面的商品数据存储在vuex里面&#xff0c; 我的…

11月29日:thinkphp框架->请求

回忆上节知识点 thinkphp官方文档解释 Rest控制器&#xff1a;主要是对资源进行控制&#xff0c;在thinkphp6.0开始废弃&#xff0c;推荐使用资源控制器 Rest控制器使用符合RESTFul风格&#xff0c;RESTFul方法和标准模式的操作方法定义主要区别在于&#xff0c;需要对请求类型…

Linux圈子里的“鲁大师“dmidecode-尚文网络xUP楠哥

~~全文共1189字&#xff0c;阅读需约5分钟。 进Q群11372462&#xff0c;领取专属报名福利&#xff0c;包含云计算学习路线图代表性实战训练大厂云计算面试题资料! Linux系统内核中有这样一个叫做DMI的东东&#xff0c;英文全称是Desktop Management Interface&#xff0c;翻译…

Ubutun搭建集群遇到的一些问题

安装部署K8s集群时会遇到很多问题&#xff0c;以下都是我踩过的坑&#xff0c;还有一些小坑当时没来得及记录&#xff0c;后续如果有遇到的话再进行补充。此处非常感谢江城琉璃梦同学对我的帮助。 1.工作节点执行kubectl get nodes时拒绝连接 执行指令&#xff1a;kubectl ge…

管理最忌讳用权管人

阅读本文大概需要 1.66 分钟。最近星球在更新一些系列课程&#xff0c;其中有一节课叫「怎样从技术人转型管理者&#xff1f;」应该很适合大多读者&#xff0c;毕竟关注我的读者里&#xff0c;做技术做管理的居多&#xff0c;所以这篇也发这里给大家分享下。程序员做技术的&…

网络结点中心性

结点中心性 node centrality 被认为是度量网络结点重要性的重要指标 常用的结点中心性有以下五种&#xff1a; &#xff08;以下各中心的概念在不同地方的定义可能不同&#xff0c;实际计算应查看使用工具的具体实现&#xff09; 1、度中心性 degree centrality 常被直接称为…