Rust 程序设计语言学习——泛型、Trait和生命周期

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

Trait 定义了某个特定类型拥有可能与其他类型共享的功能。可以通过 Trait 以一种抽象的方式定义共同行为。可以使用 trait bounds 指定泛型是任何拥有特定行为的类型。

生命周期是另一类我们已经使用过的泛型。不同于确保类型有期望的行为,生命周期确保引用如预期一直有效。

在这里插入图片描述

一、泛型

我们可以使用泛型为像函数签名或结构体这样的项创建定义,这样它们就可以用于多种不同的具体数据类型。

1.1 在函数定义中使用泛型

当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。

fn add_impl<T>(num1: T, num2: T) -> T
where
    T: std::ops::Add<Output = T> + Copy,
{
    num1 + num2
}

fn main() {
    let a = 3.0f32;
    let b = 4.5f32;

    let ret = add_impl(a, b);

    println!("The result of {} + {} is {}", a, b, ret);

    let a1 = 3;
    let b1 = 4;

    let ret1 = add_impl(a1, b1);

    println!("The result of {} + {} is {}", a1, b1, ret1);
}

为了参数化这个新函数中的这些类型,我们需要为类型参数命名,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 T,因为传统上来说,Rust 的类型参数名字都比较短,通常仅为一个字母,同时,Rust 类型名的命名规范是首字母大写驼峰式命名法(UpperCamelCase)。T 作为 “type” 的缩写是大部分 Rust 程序员的首选。

如果要在函数体中使用参数,就必须在函数签名中声明它的名字,好让编译器知道这个名字指代的是什么。同理,当在函数签名中使用一个类型参数时,必须在使用它之前就声明它。类型参数声明位于函数名称与参数列表中间的尖括号 <> 中。

运行结果

The result of 3 + 4.5 is 7.5
The result of 3 + 4 is 7

在 Rust 中,where 子句用于为泛型函数或泛型结构体指定额外的约束条件。上面的代码示例中,where 子句用于对泛型类型 T 施加两个约束:

  1. T: std::ops::Add<Output = T>:这个约束指定 T 必须实现 Add trait,并且 Add trait 的 Output 关联类型必须是 T 类型本身。std::ops::Add trait 定义了加法操作的行为,它要求实现该 trait 的类型必须提供一个 add 方法,该方法接受一个相同类型的参数并返回一个结果。这里的 <Output = T> 部分是一个 trait bound,它指定了 Add trait 的 Output 关联类型必须是 T。这意味着当你对两个 T 类型的值执行加法操作时,结果也将是 T 类型。

  2. T: Copy:这个约束指定 T 必须实现 Copy trait。Copy trait 是 Rust 中的一个标记 trait(marker trait),它指示一个类型可以被简单地复制,而不需要移动所有权或进行深拷贝。基本数字类型(如 i32, f64 等)默认实现了 Copy trait,这意味着你可以在不转移所有权的情况下复制这些类型的值。

将这两个约束结合起来,where T: std::ops::Add<Output = T> + Copy 表示 T 必须是一个可以进行加法操作并且加法结果类型与操作数类型相同,同时还可以被复制的类型。这使得 T 适用于表示那些具有自然加法操作并且可以轻松复制的值,例如整数和浮点数。

add_impl 函数示例中,这个 where 子句确保了 num1num2 可以安全地进行加法操作,并且结果可以被返回而不需要担心所有权问题,因为 T 实现了 Copy trait。这样,add 函数就可以接受任何满足这些约束的类型的参数,例如整数或浮点数,并返回它们的和。

1.2 结构体、方法定义中的泛型

同样也可以用 <> 语法来定义结构体,它包含一个或多个泛型参数类型字段。其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。

下面的示例展示了如何在结构体中使用泛型来创建灵活的数据结构,以及如何为这些结构体实现方法来执行特定操作。通过使用泛型,你可以编写更通用、更灵活的代码,这些代码可以与多种不同的数据类型一起工作。

// 定义一个泛型结构体 `Box<T>`,其中 `T` 是泛型参数
struct Box<T> {
    width: T,
    height: T,
}

// 为 `Box` 结构体实现一个方法来计算面积
impl<T> Box<T> {
    // 这个方法接受一个 `Box` 实例并返回其面积
    // 这里我们使用泛型参数 `T`,假设它实现了 `std::ops::Mul` 和 Copy
    fn area(&self) -> T
    where
        T: std::ops::Mul<Output = T> + Copy,
    {
        self.width * self.height // 面积 = 长 * 宽
    }
}

// 为 `Box` 结构体实现一个新方法,用于创建新的实例
impl<T> Box<T> {
    fn new(width: T, height: T) -> Self {
        Box { width, height }
    }
}

fn main() {
    // 创建一个整数类型的 `Box`
    let int_box = Box::new(10, 20);
    println!("The area of the integer box is: {}", int_box.area());

    // 创建一个浮点数类型的 `Box`
    let float_box = Box::new(10.5, 20.3);
    println!("The area of the float box is: {}", float_box.area());
}

运行结果

The area of the integer box is: 200
The area of the float box is: 213.15

在这个示例中:

  1. 我们定义了一个名为 Box 的泛型结构体,它有两个字段:widthheight,它们的类型都是泛型参数 T
  2. 我们为 Box 结构体实现了一个名为 area 的方法,该方法计算并返回面积。这里我们使用了泛型参数 T 并为其添加了 trait bounds,确保 T 可以进行乘法操作。
  3. 我们还为 Box 结构体实现了一个名为 new 的关联函数(也称为静态方法),它接受宽度和高度作为参数,并返回一个新的 Box 实例。
  4. main 函数中,我们分别使用整数和浮点数类型创建了 Box 结构体的实例,并调用了 area 方法来计算和打印它们的面积。

1.3 枚举定义中的泛型

和结构体类似,枚举也可以在成员中存放泛型数据类型。比如标准库提供的 Option<T> 枚举,这里再回顾一下:

enum Option<T> {
    Some(T),
    None,
}

如你所见 Option<T> 是一个拥有泛型 T 的枚举,它有两个成员:Some,它存放了一个类型 T 的值,和不存在任何值的 None。通过 Option<T> 枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T> 是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。

当你意识到代码中定义了多个结构体或枚举,它们不一样的地方只是其中的值的类型的时候,不妨通过泛型类型来避免重复。

1.4 泛型代码的性能

泛型并不会使程序比具体类型运行得慢。Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。

比如下面使用标准库中的 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);
}

泛型 Option<T> 被编译器替换为了具体的定义。因为 Rust 会将每种情况下的泛型代码编译为具体类型,使用泛型没有运行时开销。当代码运行时,它的执行效率就跟好像手写每个具体定义的重复代码一样。这个单态化过程正是 Rust 泛型在运行时极其高效的原因。

二、Trait

trait 类似于其他语言中的常被称为接口(interfaces)的功能,虽然有一些不同。

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

// 定义一个 trait `Animal`,它有两个方法:`speak` 和 `name`
pub trait Animal {
    fn speak(&self);
    fn name(&self) -> &str;
}

// 为 `Dog` 结构体实现 `Animal` trait
struct Dog {
    name: String,
}

impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }

    fn name(&self) -> &str {
        &self.name
    }
}

// 为 `Cat` 结构体实现 `Animal` trait
struct Cat {
    name: String,
}

impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }

    fn name(&self) -> &str {
        &self.name
    }
}

// 定义一个函数,它接受任何实现了 `Animal` trait 的类型作为参数
fn animal_sound<T: Animal>(animal: &T) {
    println!("{} says: ", animal.name());
    animal.speak();
}

fn main() {
    let dog = Dog {
        name: "Rex".to_string(),
    };
    let cat = Cat {
        name: "Whiskers".to_string(),
    };

    animal_sound(&dog);
    animal_sound(&cat);
}

运行结果

Rex says: 
Woof!
Whiskers says: 
Meow!

在这个示例中:

  1. 我们定义了一个名为 Animal 的 trait,它有两个方法:speaknamespeak 方法没有返回值,而 name 方法返回一个字符串的引用。

  2. 我们定义了两个结构体:DogCat,它们都包含一个 name 字段。

  3. 我们为 DogCat 结构体分别实现了 Animal trait。对于每个结构体,我们提供了 speakname 方法的具体实现。

  4. 我们定义了一个名为 animal_sound 的泛型函数,它接受任何实现了 Animal trait 的类型作为参数。这个函数打印出动物的名字和它发出的声音。

  5. main 函数中,我们创建了 DogCat 的实例,然后使用 animal_sound 函数来打印它们的名字和声音。

这个示例展示了如何定义 trait 并为不同的结构体实现它,以及如何使用 trait bounds 来创建可以与多种实现了相同 trait 的类型一起工作的泛型函数。通过使用 trait,我们可以确保不同的类型有一致的行为,同时保持代码的灵活性和可重用性。

有时为 trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。这样当为某个特定类型实现 trait 时,可以选择保留或重载每个方法的默认行为。修改上个例子的两处后。

// name 方法实现默认行为
pub trait Animal {
    fn speak(&self);
    fn name(&self) -> &str {
        "Kate"
    }
}
...
// 去除 name 方法
impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }
}

运行结果

Rex says: 
Woof!
Kate says: 
Meow!

三、生命周期

生命周期注解甚至不是一个大部分语言都有的概念,所以这可能感觉起来有些陌生。Rust 中的每一个引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明它们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

3.1 生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用(dangling references),后者会导致程序引用了非预期引用的数据。

fn main() {
    let a;

    {
        let b = 1;
        a = &b;
        println!("a: {a} b: {b}");
    }

    println!("a: {a}");
}

外部作用域声明了一个没有初值的变量 a,而内部作用域声明了一个初值为 1 的变量 b。在内部作用域中,我们尝试将 a 的值设置为一个 b 的引用。接着在内部作用域结束后,尝试打印出 a 的值。这段代码不能编译因为 a 引用的值在尝试使用之前就离开了作用域。

编译报错信息如下:

   Compiling playground v0.0.1 (/playground)
error[E0597]: `b` does not live long enough
  --> src/main.rs:6:13
   |
5  |         let b = 1;
   |             - binding `b` declared here
6  |         a = &b;
   |             ^^ borrowed value does not live long enough
7  |         println!("a: {a} b: {b}");
8  |     }
   |     - `b` dropped here while still borrowed
9  |
10 |     println!("a: {a}");
   |                  --- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` (bin "playground") due to 1 previous error

借用检查器

Rust 编译器有一个借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。

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

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

3.2 函数中的泛型生命周期

比如下面的例子 max 函数用来比较入参 x 和 y 中的最大值,不过我们使用了引用。

fn max(x: &i32, y: &i32) -> &i32 {
    if (*x) > (*y) {
        x
    } else {
        y
    }
}

fn main() {
    let a = 10;
    let b = 20;
    println!("max: {}", max(&a, &b));
}

编译后报错:

   Compiling playground v0.0.1 (/playground)
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:29
  |
1 | fn max(x: &i32, y: &i32) -> &i32 {
  |           ----     ----     ^ 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
  |
1 | fn max<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  |       ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `playground` (bin "playground") due to 1 previous error

编译报错信息中的关键点:
缺少生命周期说明符。
此函数的返回类型包含一个借用值,但签名没有说明它是从x还是y借用的。
考虑引入一个命名生命周期参数。

提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 x 或 y。事实上我们也不知道,因为函数体中 if 块返回一个 x 的引用而 else 块返回一个 y 的引用。

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

生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短。相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。与当函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。大多数人使用 'a 作为第一个生命周期注解。生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

这里有一些例子:我们有一个没有生命周期参数的 i32 的引用,一个有叫做 'a 的生命周期参数的 i32 的引用,和一个生命周期也是 'a 的 i32 的可变引用:

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

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

例如如果函数有一个生命周期 'a 的 i32 的引用的参数 first。还有另一个同样是生命周期 'a 的 i32 的引用的参数 second。这两个生命周期注解意味着引用 first 和 second 必须与这泛型生命周期存在得一样久。

现在来修复上例中的报错。

fn max<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
    if (*x) > (*y) {
        x
    } else {
        y
    }
}

fn main() {
    let a = 10;
    let b = 20;
    println!("max: {}", max(&a, &b));
}

运行结果

max: 20

这段代码定义了一个名为 max 的函数,该函数接受两个指向整数的引用作为参数,并返回一个指向整数的引用。在 main 函数中,创建了两个变量 a 和 b,并将它们的引用传递给 max 函数。

让我们详细分析生命周期:

  1. max 函数中,我们有两个输入引用 x 和 y,它们都有相同的生命周期 'a。这意味着这两个引用必须在整个函数执行期间保持有效。
  2. max 函数返回一个引用,其生命周期与输入引用相同,即 'a
  3. main 函数中,我们创建了两个变量 a 和 b ,它们的生命周期从声明开始到 main 函数结束。然后我们将这两个变量的引用传递给 max 函数。
  4. max 函数返回一个引用,该引用指向较大的整数。由于这个引用指向的是 a 或 b 中的一个,因此它的生命周期与 a 和 b 相同。
  5. 最后,我们在 println! 宏中使用 max 函数的返回值。由于返回值是一个引用,因此在打印之前不需要解引用。当 println! 宏执行完毕后,返回的引用将被丢弃,不再使用。

3.4 结构体和方法定义中的生命周期注解

在 Rust 中,当结构体包含引用类型的字段时,需要定义生命周期参数以确保这些引用在结构体实例的生命周期内保持有效。以下是一个包含生命周期参数的结构体定义的示例:

// 定义一个结构体 `Message`,它包含一个字符串引用
struct Message<'a> {
    content: &'a str,
}

// 为 `Message` 结构体实现一个方法来打印消息内容
impl<'a> Message<'a> {
    fn print(&self) {
        println!("The message is: {}", self.content);
    }
}

fn main() {
    let text = "Hello, Rust!".to_string();
    let message = Message { content: &text };

    message.print();
}

运行结果

The message is: Hello, Rust!

在这个示例中:

  1. Message 结构体定义了一个生命周期参数 'a
  2. Message 结构体有一个字段 content,它是对字符串切片的引用 &'a str。这意味着 content 字段借用了一个字符串,并且这个借用的生命周期至少与 Message 实例的生命周期一样长。
  3. 我们为 Message 结构体实现了一个 print 方法,它使用 self 来访问 content 字段,并打印出消息内容。
  4. main 函数中,我们创建了一个 String 类型的变量 text,然后创建了一个 Message 实例 message,将 text 的引用传递给 content 字段。
  5. 由于 text 的生命周期与 main 函数相同,它也足以覆盖 message 的生命周期,因此 Rust 编译器可以保证 message 中的引用在 print 方法调用时是有效的。

这个示例展示了如何在结构体中使用生命周期参数来确保引用的有效性。通过定义生命周期参数并将其应用于引用字段,我们可以确保结构体实例在使用这些引用时,引用指向的数据仍然有效。这是 Rust 借用检查器确保内存安全的一种方式。

3.5 生命周期省略

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

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

被编码进 Rust 引用分析的模式被称为生命周期省略规则(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

第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法 (method),那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

3.6 静态生命周期

这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期,我们也可以选择像下面这样标注出来:

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

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static 的。

你可能在错误信息的帮助文本中见过使用 'static 生命周期的建议,不过将引用指定为 'static 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效,以及你是否希望它存在得这么久。大部分情况中,推荐 'static 生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个 'static 的生命周期。

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

以下是一个结合了泛型类型参数、trait bounds 和生命周期的例子。这个示例展示了如何在泛型结构体中使用生命周期参数来确保对引用的有效管理,并且展示了如何使用 trait bounds 来约束泛型参数实现特定的行为。

// 定义一个简单的 trait `Append`
trait Append {
    fn append(&mut self, other: &str);
}

// 为 `String` 类型实现 `Append` trait
impl Append for String {
    fn append(&mut self, other: &str) {
        self.push_str(other);
    }
}

// 定义一个泛型结构体 `Appender`,它包含一个实现了 `Append` trait 的类型参数 `T`
// 并且这个类型参数 `T` 有一个生命周期 `'a`
struct Appender<'a, T: Append> {
    item: &'a mut T,
}

// 为 `Appender` 结构体实现一个方法来添加内容
impl<'a, T: Append> Appender<'a, T> {
    fn add_content(&mut self, other: &str) {
        self.item.append(other);
    }
}

fn main() {
    // 创建一个 `String` 类型的实例 `text`
    let mut text = String::from("Hello, ");
    // 创建一个 `Appender` 实例,其 `item` 字段包含 `text` 的可变引用
    let mut appender = Appender { item: &mut text };
    // 调用 `add_content` 方法来添加内容到 `Appender` 实例的 `item`
    appender.add_content("world!");
    // 打印最终的字符串
    println!("{}", text);
}

运行结果

Hello, world!

在这个示例中:

  1. 我们定义了一个 Append trait,它有一个 append 方法,用于向接收者添加内容。
  2. 我们为 String 类型实现了 Append trait,使用 push_str 方法来添加字符串。
  3. 我们定义了一个泛型结构体 Appender<'a, T>,它包含一个类型为 T 的可变引用 item,其中 T 必须实现了 Append trait,并且有一个生命周期 'a。这意味着 Appender 持有的 item'a 生命周期内是有效的。
  4. 我们为 Appender 结构体实现了一个 add_content 方法,它调用 item 字段的 append 方法来添加内容。
  5. main 函数中,我们创建了一个 String 类型的实例 text,然后创建了一个 Appender 实例 appender,其 item 字段包含 text 的可变引用。
  6. 我们调用 appenderadd_content 方法来添加内容到 text
  7. 最后,我们打印出最终的字符串。

参考链接

  1. Rust 官方网站:https://www.rust-lang.org/zh-CN
  2. Rust 官方文档:https://doc.rust-lang.org/
  3. Rust Play:https://play.rust-lang.org/
  4. 《Rust 程序设计语言》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据可视化期末考试(logistic回归)

目录 1、Logistic回归 2、梯度上升算法 1、Logistic回归 假设现在有一些数据点&#xff0c;我们利用一条直线对这些点进行拟合(该线称为最佳拟合直线)&#xff0c;这个拟合过程就称作为回归&#xff0c;如下图所示&#xff1a; Logistic回归是分类方法&#xff0c;它利用的是…

用FFmpeg合并音频和视频

使用FFmpeg合并音频和视频是一个相对直接的过程。可以通过以下一些基本的步骤和命令示例完成这个任务&#xff1a; 安装FFmpeg&#xff1a;首先&#xff0c;确保你的系统中已经安装了FFmpeg。你可以从[FFmpeg官网](Download FFmpeg)下载并安装它。 准备素材&#xff1a;确保你…

压铸工艺介绍

1.压铸的主要特点 1.压铸是让有色金属熔融&#xff0c;并在加压的情况下让其流入模具中。由于使用模具&#xff0c;所以适合进行大量生产。不同于其他铸造方式&#xff0c;压铸需要加压&#xff0c;所以具有尺寸精度优秀的特点。此外&#xff0c;由于并非使用砂型而是采用模具…

如何给WPS、Word、PPT等办公三件套添加收费字体---方正仿宋GBK

1.先下载需要的字体。 下载字体的网站比较多&#xff0c;基本上都是免费的。随便在网上搜索一个就可以了&#xff0c;下面是下载的链接。 方正仿宋GBK字体免费下载和在线预览-字体天下 ​www.fonts.net.cn/font-31602268591.html 注意&#xff1a;切记不要商用&#xff0c;以免…

基于weixin小程序农场驿站系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;农场资讯管理&#xff0c;用户管理&#xff0c;卖家管理&#xff0c;用户分享管理&#xff0c;分享类型管理&#xff0c;商品信息管理&#xff0c;商品类型管理 开发系统&#xff1a;Windows 架构模式&…

高频科技亮相SEMl-e2024第六届深圳国际半导体展,以超纯工艺推动行业发展

6月26-28日,SEMl-e2024第六届深圳国际半导体展在深圳国际会展中心(宝安新馆)隆重举办。本次展会以【“芯”中有“算”智享未来】为主题,汇聚800多家展商,集中展示了集成电路、电子元器件、第三代半导体及产业链材料和设备为一体的半导体产业链,搭建了供需精准对接、探索行业新发…

【多线程】如何解决线程安全问题?

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. synchronized 关键字1.1 锁是什么1.2 如何加锁1.3 synchronized 修饰方法1) 修饰普通成员方法2) 修饰静态…

web前端——css(一篇教会网页制作)

目录 一、基本语法 1.行内样式表 2.内嵌样式表 3.外部样式表 二、选择器 1.标签选择器 2.类选择器 3.id 选择器 4.通配选择器 三、常见修饰 1.文本 2.背景 3.列表 4.伪类 5.透明度 6.块级、行级、行级块标签 7.div 和 span 四、盒子模型&#xff08;重点&…

Python学习打卡:day15

day15 笔记来源于&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了 目录 day15110、数据分析案例步骤 1 —— 文件读取data_define_108.pyfile_define_108.py 111、数据分析案例步骤二——数据计算112、数据分析案例步骤…

基于LMS自适应滤波的窄带信号宽带噪声去除(MATLAB R2021B)

数十年的研究极大的发展了自适应滤波理论&#xff0c;自适应滤波理论已经成为信号处理领域研究的热点之一。从理论上讲&#xff0c;自适应滤波问题没有唯一解。为了得到自适应滤波器及其应用系统&#xff0c;可以根据不同的优化准则推导出许多不同的自适应理论。目前该理论主要…

【C语言】字符/字符串+内存函数

目录 Ⅰ、字符函数和字符串函数 1 .strlen 2.strcpy 3.strcat 4.strcmp 5.strncpy 6.strncat 7.strncmp 8.strstr 9.strtok 10.strerror 11.字符函数 12. 字符转换函数 Ⅱ、内存函数 1 .memcpy 2.memmove 3.memcmp 4.memset Ⅰ、字符函数和字符串函数 1 .strlen 函数原型&…

Java热门技术点总结:Lambda表达式与Stream API

第一部分&#xff1a;Lambda表达式 1. 简介 Lambda表达式是Java 8引入的一个非常重要的特性&#xff0c;它提供了一种简洁、灵活的函数式编程方式。Lambda表达式允许我们将函数作为参数传递&#xff0c;极大的简化了代码的编写。 2. 基本语法 Lambda表达式的基本语法如下&a…

聚类模型的算法性能评价

一、概述 作为机器学习领域的重要内容之一&#xff0c;聚类模型在许多方面能够发挥举足轻重的作用。所谓聚类&#xff0c;就是通过一定的技术方法将一堆数据样本依照其特性划分为不同的簇类&#xff0c;使得同一个簇内的样本有着更相近的属性。依不同的实现策略&#xff0c;聚类…

目标检测之YoloV1

一、预测阶段&#xff08;前向推断&#xff09; 在预测阶段Yolo就相当于一个黑箱子&#xff0c;输入的是448*448*3的图像&#xff0c;输出是7*7*30的张量&#xff0c;包含了所有预测框的坐标、置信度和类别 为什么是7*7*30呢&#xff1f; --将输入图像划分成s*s个grid cell&a…

stm32 No traget connected/debug识别不到串口的问题

关于stm32串口识别不到&#xff0c;第一步先确定是否线接错&#xff08;stlink与stm32接口对应&#xff09;&#xff0c;如果确认接线没有问题的话&#xff0c;可以使用以下方法&#xff0c;成功率较高。 首先将stlink的boot0置1&#xff0c;就是把跳线帽换到高电平这一侧&…

专业技术!最新氧化物异质结纳米制备技术

网盘 https://pan.baidu.com/s/1vjO2yLxm638YpnqDQmX7-g?pwd3at5 MOF衍生的B_A_B结构氧化物异质结及其制备方法和应用.pdf 二硫化钼-硫化镉纳米复合材料及其制备方法和应用.pdf 具有异质界面的耐辐照复合薄膜及其制备方法与应用.pdf 基于异质结双界面层纳米材料的复合介电薄膜…

基于单片机和LabVIEW 的远程矿井水位监控系统设计

摘要 &#xff1a; 针 对 现 有 矿 井 水 位 监 控 系 统 存 在 结 构 复 杂 和 不 能 远 程 监 控 的 问 题 &#xff0c; 设计了基于单片机和&#xff2c;&#xff41;&#xff42;&#xff36;&#xff29;&#xff25;&#xff37; 的远程矿井水位监控系统 &#xff0c; 详…

获取当前操作系统的名称platform.system()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 获取当前操作系统的名称 platform.system() [太阳]选择题 在Python中&#xff0c;platform.system() 函数被用来获取什么信息&#xff1f; import platform print("【执行】platform.s…

Python数据分析第二课:conda的基础命令

Python数据分析第二课&#xff1a;conda的基础命令 1.conda是什么? conda是一个开源的包管理系统&#xff0c;可以帮助我们进行管理多个不同版本的软件包&#xff0c;还可以帮助我们建立虚拟环境&#xff0c;以便对不同的项目进行隔离。 简单来说&#xff0c;conda是一个软…

充电站,正在杀死加油站

最近&#xff0c;深圳公布了一组数据&#xff0c;深圳的超级充电站数量已超过传统加油站数量&#xff0c;充电枪数量也已超过加油枪数量。 从全国范围看&#xff0c;加油站关停的速度在加快。 充电站正在杀死加油站。 加油站&#xff0c;未来何去何从&#xff1f; 01. 减少 我…