【跟小嘉学 Rust 编程】十、泛型(Generic Type)、特征(Trait)和生命周期(Lifetimes)

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十、泛型(Generic Type)、特征(Trait)和生命周期(Lifetimes)

前言

每一门编程语言都有有效处理重复的工具。我们可以使用泛型来定义具体类型或其他属性的抽象替代品。

主要教材参考 《The Rust Programming Language》


一、泛型数据类型(Generic Data Types)

1.1、使用函数来消除重复代码

范例:求最大值

fn max(list: &[i32]) ->i32{
    
    
    let mut largest = list[0];
    // for &item in list  {
    
    
    //     if item> largest {
    
    
    //         largest =  item;
    //     }
    // }
    for item in list  {
    
    
        if *item> largest {
    
    
            largest =  *item;
        }
    }
    largest
}

fn main() {
    
    
    let result  = max(&vec![34,50,25,100,65]);
    println!("{:?}", result);
    let result = max(&vec![102,34,6000,189,54]);
    println!("{:?}", result);
}

需要注意的是:注释部分的for 循环和下列循环的作用是相同的,*的作用表示解引用。

1.2、泛型函数

函数中的泛型主要用于定义参数和返回值的类型。

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

此时编译会有如下的编译错误

my-project % cargo run
   Compiling my-project v0.1.0 (~/Desktop/code/rust_code/my-project)
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 `my-project` (bin "my-project") due to previous error

错误提示说,对于 T 可能是的所有的类型,该方法并不适用,为了能够是使用>比较,笔试在类型上实现
std::cmp::PartialOrd 这个 接口。

 fn largest<T: std::cmp::PartialOrd>(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);
}

1.3、泛型和结构体

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

1.3、泛型和枚举

1.3.1、Option 枚举

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

1.3.2、Result 枚举

enum Result<T, E> {
    
    
    Ok(T),
    Err(E),
}

1.4、泛型和结构体方法

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

1.5、泛型代码的性能

使用泛型的代码和使用具体类型的代码运行速度是一样的。大致来说编译器实现泛型的两种方式

  • 装箱(Boxing)
    • 类型擦除(Type-erased):java
      • 对Java来说统一的数据类型就是Object,在编译阶段做完类型检查后就将类型信息通过转换成Object进行擦除,这样只需要生成一份泛型函数的副本即可。类型擦除保证了泛型函数生成的字节码和非泛型函数的是相同的,也符合Java对兼容性的要求。不过类型擦除也给Java的泛型带来了很多的限制。
    • 虚函数表(VTable) : Embedded vtable Java
    • 字段传递(Dictonary passing): Go 、Withness table Switf
  • 单态化(Monomorphization)
    • 代码生成(Code Generation) C
      • 这种技术是编译器不支持泛型时可采用的最简单的方案,比如C语言的宏就可以实现代码生成泛型函数的功能。不过这种技术实现的泛型有很多边界问题和限制,只有在编程语言不支持泛型特性的情况下才会使用。
    • 模板(Template) C++
      • C++通过模板实现泛型类、方法和函数,这导致编译器为每个唯一的类型参数集编译了代码的单独副本。这种方法的一个关键优势是没有运行时性能开销,尽管它以增加二进制文件大小和编译时间为代价
    • 蜡印(Stenclling): GC Shape Stencilling GO
      • 蜡印其实就是模版,也是一种代码生成技术。但Go除了使用字典传递实现装箱外,还采用了GC Shape Stenciling的技术

装箱的思路就是将所有类型包装成统一的类型,有了统一的类型就有了统一的内存模型,这样函数在调用时传递的就是统一的数据类型,也就不会出现类型不匹配的问题。

Rust 在编译的时候执行单态化(Monomorphization),单态化的思路就是自动生成多个类型的泛型函数版本,看起来就是一个模板代码的生成的过哦差,但是也需要考虑很多种情况。

  • 生成所有类型的函数版本:这种最简单,但是会拖慢编译时间,也会让最终的二进制文件变得很庞大。
  • 生成调用类型的函数版本:这种需要编译器分阶段或多次编译,比如需要遍历寻找调用点来确定最终的类型列表,对于不同包的同名函数的处理等。
  • 是否支持独立编译:如果调用泛型函数的类型与泛型函数不在同一个包内,是否能支持泛型函数独立的编译。

二、特征(Traits): 定义共享行为

Trait 告诉 Rust 编译器某种类型具有哪些并且可以与其他类型共享的功能。Trait 是抽象的定义共享行为。Trait bounds (约束):泛型类型参数指定了为实现了特定行为的类型。在别的编程语言可以看作接口,但是有些区别。

2.1、定义 Trait

Trait 的定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为。Trait 使用关键字 trait 来定义,只有方法的定义没有具体实现,一个trait 可以有多个方法,每个方法占一行以; 结尾。

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

2.2、为类型实现 trait

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

2.3、Trait 也可以为方法提供默认实现

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

2.4、Trait 作函数参数

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

pub fn notify(item: &impl Summary) {
    
    
    println!("Breaking news! {}", item.summarize());
}

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

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

2.5、Trait 约束

  • 可以在某个类型上实现某个trait的前提条件:这个类型或者这个 trait 是在本地 crate 里面定义的
  • 无法为外部类型实现外部的trait:

2.5.1、泛型约束

pub fn notify<T: Summary>(item: &T) {
    
    
    println!("Breaking news! {}", item.summarize());
}

2.5.2、使用 + 定义多个trait 约束

use std::fmt::Display;
pub fn notify01(item: impl Summary + Display) {
    
    
    println!("Breaking news! {}", item.summarize());
}
pub fn notify02<T: Summary + Display>(item: T) {
    
    
    println!("Breaking news! {}", item.summarize());
}

2.5.3、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,
{
    
    

三、变量的生命周期

3.1、生命周期介绍

  • Rust 的每个引用都有自己的生命周期。
  • 生命周期:引用保持有效的作用域。
  • 大多数情况:生命周期是隐式的、可被推断的
  • 当引用的生命周期可能以不同的方式互相关联时,手动标注生命周期。

生命周期的主要目标就是:避免悬垂引用(dangling reference)

3.2、借用检查器

Rust 编译器的借用检查器:比较作用域来判断所有的借用是否合法。

3.3、函数签名上的生命周期标注

我们来看下面代码。


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

    let result = longest(string1.as_str(), string2);
}
fn longest(x: &str, y: &str) -> &str{
    
    
    if x.len() > y.len() {
    
    
        x
    }else {
    
    
        y
    }
}

上述代码将会报错

raojiamin@192 my-project % cargo run
   Compiling my-project v0.1.0 (/Users/raojiamin/Desktop/code/rust_code/my-project)
error[E0106]: missing lifetime specifier
 --> src/main.rs:8:33
  |
8 | fn longest(x: &str, y: &str) -> &str{
    
    
  |               ----     ----     ^ 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
  |
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{
    
    
  |           ++++     ++          ++          ++

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

根据错误提示可以看出借用检查器不知道返回值类型包含借用的值是使用哪个生命周期。


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

    let result = longest(string1.as_str(), string2);
    println!("result is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{
    
    
    if x.len() > y.len() {
    
    
        x
    }else {
    
    
        y
    }
}

3.4、生命周期标注

  • 生命周期的标注不会改变引用的生命周期长度;
  • 当指定来泛型生命周期参数,函数可以接收带有任何生命周期的引用;
  • 生命周期的标注:描述了多个引用的生命周期的关系,但是不会影响生命周期;

语法:

  • 生命周期参数名以单引号(')开头,通常全小写且非常短,很多人使用'a
  • 生命周期的位置:在引用&符号后,使用空格将标注和引用类型分开;
  • 例如:
 &i32 // 一个引用
 &'a i32 // 带有显式生命周期的引用
 &'a mut i32 // 带有显式生命周期的可变引用 

单个生命周期标注本身没有意义。

3.5、函数签名的生命周期标注

泛型生命周期参数声明在:函数和参数列表之间的<>里面

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

生命周期取决于的参数函数最短的一个生命周期。

3.6、深入理解生命周期

  • 指定生命周期参数的方式依赖于函数所做的事情;
  • 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配;
  • 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值;
  • 这就是悬垂引用,该值在函数结束时候就走出了作用域;
    

3.7、结构体中的生命周期标注

struct 里面可以包含自持有的类型、引用(需要在每个引用上添加生命周期标注)

3.8、生命周期的省略

早期 rust 编译器要求每个引用都需要添加生命周期标注。

在 Rust 引用分析中所需要编入的模式称为生命周期省略规则

  • 这些规则无需开发者来遵守;
  • 它们是一些特殊情况,由编译器来考虑
  • 如果你的代码符合这些情况,那么就无需显式标注生命周期

生命周期省略规则不会提供完整的推断,如果应用规则后,引用的生命周期仍然模糊不清,则出现编译错误。解决办法就是添加生命周期标注,表明引用间的相互关系;

3.9、输入/输出生命周期

  • 输入生命周期:函数/方法的参数
  • 输出生命周期:函数/方法的返回值

3.10、生命周期省略的三个规则

编译器使用3个规则在没有显式标注生命周期的情况下,来确定引用的生命周期

  • 规则1 应用于输入生命周期
  • 规则 2、3 应用于输出生命周期
  • 如果编译器应用完3个规则之后,仍然有无法确定生命周期的引用,则报错;
  • 这些规则适用于 fn 定义 和 impl 块

规则 1: 每个引用类型的参数都有自己的生命周期
规则 2: 如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
规则 3: 如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self (方法),那么 self 的生命周期会被赋给所有的输出生命周期参数

3.11、方法定义中的生命周期标注

  • 在 struct 上使用生命周期实现方法,语法和泛型参数的语法一样;
  • 在哪声明和使用生命周期参数,依赖于生命周期参数是否和字段、方法的参数或返回值有关;
  • struct 字段的生命周期名
    • 在 impl 后声明
    • 在 struct 名后声明
    • 这些生命周期是 struct 类型的一部分
  • impl 块内的方法签名中
    • 引用必须绑定于 struct 字段引用的生命周期,或引用是独立的也可以
    • 生命周期省略规则经常使得方法中的生命周期标注不是必须的;

3.12、静态生命周期

'static 是一个特殊的生命周期:整个程序的持续时间。例如:所有的字符串字面知值都拥有 'static生命周期。

为引用指定 'static 生命周期前要三思,是否需要引用在程序整个生命周期内都存活。

总结

以上就是今天要讲的内容

  • 讲解了泛型的定义和使用
  • 讲解了String类型的转换
  • 讲解了 Trait 接口使用
  • 讲解了生命周期标注以及使用

猜你喜欢

转载自blog.csdn.net/fj_Author/article/details/131481509
今日推荐