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

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

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

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

在这里插入图片描述

一、泛型

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

1.1 在函数定义中使用泛型

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

fn add_impl<T>(num1: T, num2: T) -> T
whereT: 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 子句确保了 num1 和 num2 可以安全地进行加法操作,并且结果可以被返回而不需要担心所有权问题,因为 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` 和 Copyfn area(&self) -> TwhereT: 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 的泛型结构体,它有两个字段:width 和 height,它们的类型都是泛型参数 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> 展开为两个针对 i32 和 f64 的定义,接着将泛型定义替换为这两个具体的定义。

编译器生成的单态化版本的代码看起来像这样(编译器会使用不同于如下假想的名字):

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,它有两个方法:speak 和 name。speak 方法没有返回值,而 name 方法返回一个字符串的引用。

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

  3. 我们为 Dog 和 Cat 结构体分别实现了 Animal trait。对于每个结构体,我们提供了 speak 和 name 方法的具体实现。

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

  5. 在 main 函数中,我们创建了 Dog 和 Cat 的实例,然后使用 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 hereFor 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. 我们调用 appender 的 add_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.tpcf.cn/news/862212.html

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

相关文章

Mac excel 同时冻结首行和首列

1. 选择B2窗格 2. 选择视图 3. 选择冻结窗格 最后首行和首列的分割线加粗了就表示成功了

youlai-boot项目的学习(3) 本地redis、MinIO的安装与配置

youlai-boot项目除了使用mysql数据库、还有redis&#xff0c;以及OSS服务&#xff0c;OSS除了云OSS服务&#xff0c;还有自部署的MinIO服务。 前面我们已经安装好了mysql数据库&#xff0c;那么我们来看看本地redis、MinIO服务怎么部署 环境 mac OS&#xff0c; iterm2&#…

C语言力扣刷题8——环形链表——[快慢双指针, 龟兔赛跑]

力扣刷题8——环形链表——[快慢双指针, 龟兔赛跑] 一、博客声明二、题目描述三、解题思路1、思路说明 四、解题代码&#xff08;附注释&#xff09; 一、博客声明 找工作逃不过刷题&#xff0c;为了更好的督促自己学习以及理解力扣大佬们的解题思路&#xff0c;开辟这个系列来…

分布式事务解决方案(八股)

1. CAP理论 一致性【Consistency】&#xff1a;所有节点访问最新的数据可用性【Availability】&#xff1a;非故障节点响应正常分区容错性【Partition Tolerance】&#xff1a;分布式系统出现网络分区&#xff0c;仍能对外提供服务 附&#xff1a;网络分区&#xff1a;分布式…

拳打开源SOTA脚踢商业闭源的LI-DiT是怎样炼成的?(商汤/MMLab/上海AI Lab)

文章地址&#xff1a;https://arxiv.org/pdf/2406.11831 仅基于解码器的 Transformer 的大语言模型&#xff08;LLMs&#xff09;与 CLIP 和 T5 系列模型相比&#xff0c;已经展示出卓越的文本理解能力。然而&#xff0c;在文本到图像扩散模型中利用当前先进的大语言模型的范例…

[机器学习]-3 万字话清从传统神经网络到深度学习

神经网络&#xff08;Neural Networks, NNs&#xff09;是机器学习的一种重要方法&#xff0c;灵感来源于生物神经系统&#xff0c;由大量互联的节点&#xff08;称为神经元或单元&#xff09;组成&#xff0c;通过调整这些节点间的连接权重来学习和表示复杂的非线性关系。传统…

中霖教育怎么样?注册会计师考试难吗?

中霖教育&#xff1a;注册会计师&#xff08;CPA&#xff09;考试的难度高吗&#xff1f; 对于不同背景的考生来说&#xff0c;注册会计师考试的挑战程度不同。那些有良好基础和充裕准备时间的考生&#xff0c;通过考试的可能性要超过那些从零开始且准备时间有限的人。 据最近…

GPOPS-II教程(5): 月球探测器着陆最优控制问题

文章目录 问题描述GPOPS代码main functioncontinuous functionendpoint function仿真结果 最后 问题描述 参考文献&#xff1a;[1] Meditch J. On the problem of optimal thrust programming for a lunar soft landing[J]. IEEE Transactions on Automatic Control, 1964, 9(4…

6.28学习笔记

一、马克思 1、对立的历史观&#xff1a;唯物史观、唯心史观&#xff08;不是“主义”&#xff09; 2、人口因素对社会发展起&#xff08;制约和影响&#xff09;作用 3、自然地理环境是人类社会生存和发展&#xff08;永恒的、必要的条件&#xff09; 4、哲学社会科学的发展水…

Linux基础- 使用 Apache 服务部署静态网站

目录 零. 简介 一. linux安装Apache 二. 创建网页 三. window访问 修改了一下默认端口 到 8080 零. 简介 Apache 是世界使用排名第一的 Web 服务器软件。 它具有以下一些显著特点和优势&#xff1a; 开源免费&#xff1a;可以免费使用和修改&#xff0c;拥有庞大的社区支…

Web渗透:任意文件下载

任意文件下载漏洞&#xff08;Arbitrary File Download Vulnerability&#xff09;是一种常见的Web安全漏洞&#xff0c;它允许攻击者通过修改输入参数&#xff0c;从服务器下载任意文件&#xff0c;而不仅仅是预期的文件&#xff1b;通常这种漏洞出现在处理用户输入的地方&…

343. 整数拆分(leetcode)

343. 整数拆分&#xff08;leetcode&#xff09; 题目描述 给定一个正整数 n &#xff0c;将其拆分为 k 个 正整数 的和&#xff08; k > 2 &#xff09;&#xff0c;并使这些整数的乘积最大化。返回你可以获得的最大乘积 。 示例1 输入: n 2 输出: 1 解释: 2 1 1, 1 1…

python CSSE7030

1 Introduction In this assignment, you will implement a (heavily) simplified version of the video game ”Into The Breach”. In this game players defend a set of civilian buildings from giant monsters. In order to achieve this goal, the player commands a s…

Spring MVC数据绑定和响应——复杂数据绑定(三)复杂POJO绑定

一、复杂POJO数组绑定的使用场景 使用简单POJO类型已经可以完成多数的数据绑定&#xff0c;但有时客户端请求中传递的参数比较复杂。例如&#xff0c;在用户查询订单时&#xff0c;页面传递的参数可能包括订单编号、用户名称等信息&#xff0c;这就包含了订单和用户两个对象的信…

AI进阶指南第四课,大模型优缺点研究?

在上一篇文章中&#xff0c;我主要探讨了LM模型与企业级模型的融合。 但是&#xff0c;在文末对于具体的大模型优缺点只是简单地说明了一下&#xff0c;并不细致。 因此&#xff0c;在这一节&#xff0c;我将更为细致地说明一下大模型的优缺点。 一&#xff0c;隐私安全 将L…

2018年全国大学生数学建模竞赛A题高温服装设计(含word论文和源代码资源)

文章目录 一、部分题目二、部分论文三、部分Matlab源代码问题11 求解h1h22 已知h1h2求解温度分布 问题21 求解第二层最佳厚度 四、完整word版论文和源代码&#xff08;两种获取方式&#xff09; 一、部分题目 2018 年高教社杯全国大学生数学建模竞赛题目 A 题 高温作业专用服…

vCenter- KMS Server healthy warning

- 问题摘要&#xff1a;KMS Server healthy warning. - 分析/故障排除: 检查集群配置-Security-Key Providers界面&#xff0c;发现KMS Server并没有完整配置并且目前集群内的虚机没有使用加密功能&#xff08;VM Encryption&#xff09;&#xff0c;您可以忽略该告警提示。 …

Linux C 程序 【02】创建线程

1.开发背景 上一个篇章&#xff0c;基于 RK3568 平台的基础上&#xff0c;运行了最简单的程序&#xff0c;然而我们使用了 Linux 系统&#xff0c;系统自带的多线程特性还是比较重要的&#xff0c;这个篇章主要描述线程的创建。 2.开发需求 设计实验&#xff1a; 创建一个线程…

【linux】find命令详解

Linux中的find命令用于在文件系统中搜索符合条件的文件或目录。 一.基本语法 find [path] [expression] #path:搜索的起始目录&#xff0c;如果不指定&#xff0c;默认为当前目录 #expression&#xff1a;定义搜索条件的表达式 命令选项 -name 按文件名搜索 -size …

入门JavaWeb之 JavaBean 实体类

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