Rust学习——泛型、trait和生命周期

trait,定义泛型行为的方法。可与泛型结合来将泛型限制为拥有特定行为的类型。

泛型主要用于帮助开发者确保类型拥有期望的行为。

生命周期则确保引用在我们需要他们的时候一直有效。

(生命周期,它是一类允许我们向编译器提供引用如何相互关联的泛型。(即当前变量/函数的引用在什么条件下回收))

介绍泛型前置概念

回顾一个不使用泛型的处理重复的技术:提取函数来减少重复。所以我们可以使用相同机制来提取一个泛型函数。

一、泛型数据类型

1-1. 结构体定义中的泛型

struct Point {

x: T,

y: T,

}

fn main() {

let integer = Point { x: 5, y: 10 };

let float = Point { x: 1.0, y: 4.0 };

}

1-2. 枚举定义中的泛型

enum Option {

Some(T),

None,

}

定义多个泛型:

enum Result<T, E> {

Ok(T),

Err(E),

}

1-3. 方法定义中的泛型

struct Point {

x: T,

y: T,

}

impl Point {

fn x(&self) -> &T {

&self.x

}

}

fn main() {

let p = Point { x: 5, y: 10 };

println!("p.x = {}", p.x());

}

注意必须在 impl 后面声明 T,这样就可以在 Point 上实现的方法中使用它了。在 impl 之后声明泛型 T ,这样 Rust 就知道 Point 的尖括号中的类型是泛型而不是具体类型。

注意:结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。没有硬性要求。

1-4. 泛型代码的性能

Rust通过在编译时进行泛型代码的单态化来保证效率。

单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。(与宏类似,只能说类似)

编译器寻找所有泛型代码被调用的位置并使用泛型代码针对具体类型生成代码。(把去重复变为重复)

二、trait:定义共享的行为

trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。

2-1. 为类型实现trait

2-2. 默认实现

我理解是trait既可以作为方法签名的集合,也可以定义默认实现,默认实现可以作用这类行为需要的通用方法.

2-3. trait 作为参数

impl Trait 语法:

pub fn notify(item: impl Summary) {

println!("Breaking news! {}", item.summarize());

}

Trait Bound 语法

impl Trait适用于直观的例子,它实际上是一种较长形式语法的语法糖,我们称为trait bound。看起来像这样:

pub fn notify<T: Summary>(item: T) {

println!("Breaking news! {}", item.summarize());

}

通过 + 指定多个trait bound

impl trait写法:

pub fn notify(item: impl Summary + Display) {

trait bound写法:

pub fn notify<T: Summary + Display>(item: T) {

通过where简化trait bound

主要是因为trait bound可能写太长,导致函数签名难以阅读,所以推出where来简化写法

原始:

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

简化后:

fn some_function<T, U>(t: T, u: U) -> i32

where T: Display + Clone,

U: Clone + Debug

{

返回实现了trait的类型

返回一个只是指定了需要实现的 trait 的类型的能力在闭包和迭代器场景十分的有用

impl Trait 允许你简单的指定函数返回一个 Iterator 而无需写出实际的冗长的类型。

通过trait bound有条件地实现方法

https://kaisery.github.io/trpl-zh-cn/ch10-02-traits.html#使用-trait-bound-有条件地实现方法

三、生命周期与引用有效性

Rust中每一个引用都有其生命周期。

*大部分时候生命周期是隐含并可以推断的。

生命周期用于保证运行时使用的引用绝对是有效的。

3-1. 生命周期避免了悬垂引用

*悬垂引用会导致程序引用了非预期引用的数据。

*作用域越大就等同于“存在的越久”

借用检查器

检查器通过比较作用域的方式来确保所有的借用都是有效的。

3-2. 函数中的泛型生命周期

https://kaisery.github.io/trpl-zh-cn/ch10-03-lifetime-syntax.html#函数中的泛型生命周期

3-3. 生命周期注解语法

*生命周期注解并不改变任何引用的生命周期的长短。

生命周期注解描述了多个引用生命周期相互的关系,而不影响其生命周期。

语法:

以 ' 撇号开头,其名称通常是小写。类似于:

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

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

3-4. 函数签名中的生命周期注解

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {

if x.len() > y.len() {

x

} else {

y

}

}

上例函数定义指定了签名中所有的引用必须有相同的生命周期 'a

*记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。

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

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

3-5. 深入理解生命周期

生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。

一旦他们形成了某种关联,Rust就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

3-6. 结构体定义中的生命周期注解

*一个存放引用的结构体,其定义需要生命周期注解

3-7. 生命周期省略(lifetime elision)

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

输入生命周期:函数或方法的参数的生命周期

输出生命周期:返回值的生命周期

何时不使用明确的生命周期注解?

应用编译器三条规则:

  1. 每一个是引用的参数都有它自己的生命周期参数(即每个引用参数生命周期注解不一样)
  2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  3. 如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self,则说明是个对象的方法(如结构体定义方法),那么所有输出生命周期参数被赋予self 的生命周期。(这条规则意味着我们经常不需要在方法签名中标注生命周期。)

***若上述三条规则不能计算出返回值类型,则需要显示生命周期注解。

3-8. 方法定义中的生命周期注解

套用规则三

3-9. 静态生命周期

'static

静态生命周期能够存活于整个程序期间。

*所有的字符串字面值都拥有 'static 生命周期

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

// eq to.

let s = "I have a static lifetime.";