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

news2024/10/7 14:27:03

每一种编程语言都有高效处理重复概念的工具。在 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.coloradmin.cn/o/1871750.html

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

相关文章

Java——IO流(一)-(8/8):释放资源-try-catch-finally、try-catch-resource

目录 try-catch-finally 介绍 实例演示1 实例演示2 try-catch-resource 介绍 实例演示 try-catch-finally 介绍 普通的释放流的方法可能会因中间的异常或是其他原因&#xff0c;导致程序执行不到释放流的代码就结束了&#xff0c;会有资源浪费的风险&#xff0c;所以建…

入门JavaWeb之 JavaBean 实体类

JavaBean 有特定写法&#xff1a; 1.必须有一个无参构造 2.属性必须私有 3.必须有对应的 get/set 方法 一般用来和数据库的字段做映射 ORM&#xff1a;对象关系映射 表->类 字段->属性 行记录->对象 连接数据库 没有的话去 Settings -> Plugins 搜索 Data…

JavaScript--js基础(详细 全面)

目录 前言: JavaScript 是什么&#xff1f;JavaScript 简介 1.JavaScript历史 2.JavaScript 具有以下特点 第一个JavaScript程序 1.在脚本文件中编写JavaScript代码 2.JavaScript代码执行顺序 基本语法 1.变量 2.数据类型 3.算术运算符 4.赋值运算 5.字符串运算符 6…

GoSync+华为智能穿戴使用指导

GoSync官方简介&#xff1a; GoSync 是一款免费应用程序&#xff0c;主要用于将您的可穿戴设备中的步行、跑步、骑自行车和游泳等活动数据同步到您的 Google Fit 和其他健身平台。在开始同步数据之前&#xff0c;您需要将您的可穿戴设备账户与您的健身平台账户连接起来。在创建…

Modbus为何要转成EtherCAT

1. Modbus是什么&#xff1f; Modbus是一种工业通信协议&#xff0c;广泛应用于工业自动化领域。它支持多种通信方式&#xff0c;包括RS-232、RS-485和TCP/IP等。Modbus协议简单易用&#xff0c;能够实现设备之间的数据交换和控制命令的传输。然而&#xff0c;它在数据传输速率…

微软推出最新视觉基础模型Florence-2 可在浏览器运行

据微软官方消息&#xff0c;微软推出视觉基础模型Florence-2&#xff0c;该模型现已能够在支持WebGPU的浏览器中100%本地运行。Florence-2-base-ft是一个拥有2.3亿参数的视觉基础模型&#xff0c;采用基于提示的方法来处理广泛的视觉和视觉语言任务。 该模型支持多种功能&…

FME实现批量合并shapefile文件数据,并提取原文件名,输出到属性表字段中的解决方法

目录 一、实现效果 二、实现过程 1.读取数据 2.暴露文件名属性 3.设置文件名字段 4.输出成果 5.模板的使用 三、总结 今天来介绍如何使用FME软件来实现对多个shapefile数据进行批量合并&#xff0c;同时提取原文件名并存储到合并后shapefile数据属性表字段中的方法&…

由监官要求下架docker hub镜像导致无法正常拉取镜像

问题&#xff1a;下载docker镜像超时 error pulling image configuration: download failed after attempts6: dial tcp 202.160.128.205:443: i/o timeout解决办法&#xff1a;配置daemon.json [rootbogon aihuidi]# cat /etc/docker/daemon.json {"registry-mirrors&qu…

<项目> 日志系统

目录 前言&#xff1a; 一、项目介绍 二、为什么需要日志系统 三、日志系统技术实现 &#xff08;一&#xff09;同步写⽇志 &#xff08;二&#xff09;异步写⽇志 四、前置技术学习 &#xff08;一&#xff09;不定参宏函数 &#xff08;二&#xff09;C语言不定参数…

mysql安装创建数据库防止踩坑

为了安装MySQL的家人们走弯路&#xff0c;稍微有些啰嗦&#xff0c;讲述我安装的时遇到的问题&#xff0c;如何解决。仔细看看离成功不远。 mysql下载链接 MySQL :: Download MySQL Community Server windows下安装mysql-8.0.29-winx64&#xff0c;下载安装包后解压到文件夹中…

摄影后期色彩管理流程(Lightroom篇)

在摄影后期处理中&#xff0c;色彩管理是确保图像从捕捉到输出的一致性和准确性的关键。Lightroom 和 Photoshop 其实已经将这套色彩管理流程作为默认选项&#xff0c;如果实质操作时仍存在色彩偏差的问题&#xff0c;可参考以下内容。 ProPhoto RGB > Adobe RGB > sRGB …

clickhouse count和uniqCombined

count(distinct ) 和 uniqCombined 获取去重后的总数。 去重&#xff1a;order by distinct argMax group by 哪个好&#xff1f;&#xff1f; clickhouse数据去重函数介绍&#xff08;count distinct&#xff09;_clickhouse distinct-CSDN博客

[论文阅读笔记33] Matching Anything by Segmenting Anything (CVPR2024 highlight)

这篇文章借助SAM模型强大的泛化性&#xff0c;在任意域上进行任意的多目标跟踪&#xff0c;而无需任何额外的标注。 其核心思想就是在训练的过程中&#xff0c;利用strong augmentation对一张图片进行变换&#xff0c;然后用SAM分割出其中的对象&#xff0c;因此可以找到一组图…

注意力机制在大语言模型中的应用

在大语言模型中&#xff0c;注意力机制&#xff08;Attention Mechanism&#xff09;用于捕获输入序列中不同标记&#xff08;token&#xff09;之间的关系和依赖性。这种机制可以动态地调整每个标记对当前处理任务的重要性&#xff0c;从而提高模型的性能。具体来说&#xff0…

Qt通过句柄获取其它进程控件实例

1.通过spy获取想要获取控件的句柄id 通过spy获取另一个软件的文本框的句柄 2.Qt写代码&#xff0c; 根据句柄获取文本框的内容 void getTextFromExternalWindow(HWND hwnd) {const int bufferSize 256;TCHAR buffer[bufferSize];// 获取窗口文本内容int length GetWindowT…

svn明明都在环境变量中添加了,但还是无法在cmd中生效

svn明明都在环境变量中添加了&#xff0c;但还是无法在cmd中生效 cmd显示原因问题解决 cmd显示 svn不是内部或外部命令&#xff0c;也不是可运行的程序或批处理文件 原因 安装svn一直点下一步下一步…&#xff0c;没有勾选command line client。 问题解决 1.按下winx&…

CSS|04 复合选择器伪类选择器属性选择器美化超链接

基本选择器&#xff1a;见上篇基本选择器 复合选择器选择器1,选择器2{属性:值;} 多元素选择器&#xff0c;同时匹配选择器1和选择器2&#xff0c;多个选择器之间用逗号分隔举例&#xff1a; p,h1,h2{margin:0px;}E F{属性:值;} 后代元素选择器&#xff0c;匹配所有属于E元素后…

Linux实用命令练习

目录 一、常用命令 二、系统命令 三、用户和组 四、权限 五、文件相关命令 六、查找 七、正则表达式 八、输入输出重定向 九、进程控制 十、其他命令 1、远程文件复制&#xff1a;scp 2、locate查找 3、which命令 4、设置或显示环境变量&#xff1a;export 5、修…

解决所有终端中文输出乱码的问题

一、系统自带的cmd.exe 以及 Git的bash.exe、sh.exe、git-bash.exe和git-cmd.exe&#xff0c;和PowerShell默认使用“当前系统区域设置”设定好的936 (ANSI/OEM - 简体中文 GBK)语言编码。 1、[当前代码页] 的936 (ANSI/OEM - 简体中文 GBK) 是导致中文乱码的原因 在控制面板→…

[分布式网络通讯框架]----Protobuf安装配置--附带每一步截图

Protobuf Protobuf&#xff08;Protocol Buffers&#xff09;协议是一种由 Google 开发的二进制序列化格式和相关的技术&#xff0c;它用于高效地序列化和反序列化结构化数据&#xff0c;通常用于网络通信、数据存储等场景。 为什么要使用Protobuf Protobuf 在许多领域都得到…