第N次入门Rust - 9.泛型、trait和生命周期

文章目录

  • 前言
  • 9.1 泛型
    • 9.1.1 泛型的解释
    • 9.1.2 泛型语法
      • 泛型函数
      • 泛型结构体
      • 泛型枚举
      • 泛型方法
      • 泛型容器
  • 9.2 Trait
    • 9.2.1 trait定义与实现
      • 定义
      • 实现
    • 9.2.2 trait作为参数和返回类型
    • 9.2.3 对泛型进行trait约束
      • 基础用法
      • 根据trait约束实现方法
    • 9.2.3 trait的继承
    • 9.2.4 Self 类型
    • 9.2.5 标准库常用trait
      • 格式化输出:Debug与Display
      • 等值比较:Eq和PartialEq
      • 次序比较:Ord与PartialOrd
      • 复制值:Clone与Copy
      • 默认值:Default
  • 9.3 生命周期(lifetime)
    • 9.3.1 基础
    • 9.3.2 静态生命周期
    • 9.3.3 生命周期与函数
    • 9.3.4 生命周期与结构体
    • 9.3.5 (重要)生命周期省略规则


前言

这一篇介绍Rust的核心:泛型、trait和生命周期~


9.1 泛型

9.1.1 泛型的解释

  • 泛型:提高代码复用能力,处理重复代码的问题。
  • Rust的泛型并不像Java的泛型,而是像C++的模板,里面有一些“占位符”,编译器在编译时将“占位符”替换为具体的类型。
  • 编译器将泛型模板中的占位符使用具体的类型替换的过程称为“单态化”。
  • 泛型代码的性能:Rust会在编译时进行反省代码的单态化(monomorphization),即在编译时即能计算出具体的类型,并转化为具体类型的方法,比如Option会被转化为Option_i32一样。也就是说,Rust的泛型更像C++的模板,不需要担心运行时的性能损耗。

9.1.2 泛型语法

  • Rust使用语法表示泛型类型,其中T可以代表任意数据类型。
  • 标准库提供Option泛型枚举,表示可能有值,也可能无值这一抽象概念:
    enum Option<T> {
        Some(T),
        None,
    }
    
    • Some(T)表示可能的值可以是任意类型T
    • None表示不存在任何值。
    • Option类型会被Rust自动引入,因此不需要显式引入作用域,直接使用Some(T)None

泛型函数

  • 当定义泛型函数时需要将泛型类型参数放在函数签名里面,这些泛型类型参数通常用来指定入参类型或返回类型
  • 具体定义方式为在函数名后面加上尖括号,然后在类括号中给出类型参数列表,最后在需要使用类型参数的地方使用类型参数。
  • 定义和使用语法:
    fn function_name<T1, T2>(val1: T1, val2: &T2, val3: &[T2]) -> T1 {
        // ...
    }
    
    let f1 = function_name(参数列表)                // 根据参数列表中的类型自动推导
    let f2 = function_name::<i32,i32>(参数列表)     // 手动给出类型
    
    • 其中为泛型列表,在声明了有哪些泛型以后即可在参数列表、返回值以及函数体中使用泛型。
  • 使用泛型函数时,直接像使用普通函数那样即可,编译器可以根据输入输出自动推导出泛型类型。
  • 如果在使用泛型函数时一定要手动给出泛型列表,需要以function_time::(参数列表)的形式给出。

泛型结构体

  • 当定义泛型函数时需要将泛型类型参数放在结构体名称后面,使用尖括号包裹,这些泛型类型参数通常用来指定结构体中字段的类型
  • 定义和使用语法:
    struct StructName<T> {
        x: T,
        y: T,
    }
    
    let s1 = StructName{x: 10, y: 10};              // T被推导为i32
    let s2 = StructName{x: 1.0, y: 1.0};            // T被推导为f32
    let s2 = StructName::<f64>{x: 1.0, y: 1.0};     // 手动给出类型
    

泛型枚举

  • 在枚举中使用泛型,主要是让枚举的变体持有泛型数据类型。
    • 例如:OptionResult
  • 具体定义方式为在枚举类型名后面加上尖括号,然后在类括号中给出类型参数列表,在变体声明中使用类型参数,当然也可以不使用类型参数。
  • 语法:
    enum EnumName<T> {
        E1(T),
        E2,
    }
    

泛型方法

  • 在为结构体或枚举实现方法的时候,可以在方法定义中使用泛型。
// 定义使用泛型的结构体
struct StructName<T> {
      x: T,
      y: T,
}

// 泛型实现:实现泛型方法
// 需要在impl关键字后面带上,然后在结构体名后也要带上
// impl表示在impl声明的作用域内有哪些通配符
// 结构体名后面的表示指定结构体的泛型使用哪种类型实现
impl<T> StructName<T> {
	fn method1(&self) -> &T { /* 具体实现 */ }
	fn method2(&self, v: T) { /* 具体实现 */ }
}

// 具体实现:实现指定类型方法的方法
// 结构体后面的表示使用i32作为泛型结构体的具体实现
impl StructName<i32> {
	fn method3(&self) -> i32 { /* 具体实现 */ }
	fn method4(&self) -> i32 { /* 具体实现 */ }
}

fn main() {
    let p = StructName{x:5,y:10};
    println!("p.x={}",p.x);
    println!("{}", p.method1());
    println!("{}", p.method3());
}
  • 个人理解:impl的作用于是impl是整个实现块,包括结构体的部分,因此对于像impl StructName {} 这样的声明,实际上第一个T是指定了实现块内的占位符T,第二个T(结构体的T)是表示结构体使用implT作为其实现。
  • 如果一个自定义泛型类型的方法定义中同时有两个以上的方法实现区块,既有泛型的方法实现区块,又有具体类型的方法实现区块,则:具体类型的方法实现区块中的方法名与泛型方法实现区块的方法名不能重复(因为这就相当于有两个方法签名一样的方法了)。
  • 相当于在这个自定义类型单态化后:
    • 一般类型都有泛型方法实现区块中的所有方法
    • 具体类型既有泛型方法实现区块中的所有方法,又有对应类型实现区块中的所有方法。
  • 不同方法实现区块的关系应该是一个并集,而不是交集,即一个方法实现区块的方法不会取代其他方法实现区块的方法。
  • 结构体里的泛型类型参数可以和方法的泛型类型参数不同。 此时方法中可以使用结构体里的泛型类型参数,也可以使用方法自己的泛型类型参数。

泛型容器

let mut v1: Vec<i32> = vec![10, 20];
let mut v2 = Vec::<32>::new();

9.2 Trait

  • trait的本质是一组方法原型,是实现某些目的的行为集合,它抽象地定义共享行为。
  • trait告诉Rust编译器:某种类型具有哪些并且可以与其它类型共享的功能。
  • trait bounds(约束):将泛型类型参数指定为实现了特定行为的类型。
  • trait与其它语言的接口(interface)类似,但有些区别。

9.2.1 trait定义与实现

定义

从语法上来说,trait可以包含两种形式的方法:

  • 抽象方法(没有具体实现的方法):结构体必须实现。抽象方法没有方法体,且方法签名后跟上;
  • 具体方法(带有具体实现的方法):默认方法,结构体不实现则使用的是默认方法,需要有自己的实现则重载方法。
// 一个trait例子
trait Geometry {
   fn area(&self) -> f32;			// 抽象方法
   fn perimeter(&self) -> f32;		// 抽象方法
   fn get_msg(&self) -> str {		// 具体方法
       format!("{} - {}", self.area(), self.perimeter())
   }
}

实现

假如有一个类型MyType,需要实现一个trait MyTrait,则使用impl MyTrait for MyType语法(表示MyType实现MyTrait特质)。在impl块中,先使用trait定义的方法签名,再在方法体内编写具体的行为。

struct MyType {
}

trait MyTrait {
	fn method1(&self);
}

impl MyTrait for MyType {
	fn method1(&self) { /* 具体实现 */ }
}

例子:

struct Rectangle {
    width: f32,
    height: f32,
}

impl Geometry for Rectangle {
    fn area(&self) -> f32 {
        self.width * self.height
    }
    
    fn perimeter(&self) -> f32 {
        (self.width + self.height) * 2.0
    }
}

9.2.2 trait作为参数和返回类型

  • trait作为参数的两种常用方式:
    • 使用impl Trait语法表示参数类型
    • 使用trait对泛型参数进行约束
  • impl Trait:假如有一个特质MyTrait,有一个函数的一个入参或出参为实现了MyTrait的参数,则参数类型为impl MyTrait,如果需要入参出参同时实现TraitATraitB,可以使用impl TraitA + TraitB代表该参数的类型:
    fn function_name1(param: impl MyTrait) {	// 接收实现了MyTrait的入参
        // 函数体
    }
    
    fn function_name2(param: impl TraitA + TraitB) {	// 接收实现了TraitA和TraitB的入参
        // 函数体
    }
    
    fn function_name3() -> impl MyTrait {			// 返回实现了MyTrait的入参
        // 函数体
    }
    

9.2.3 对泛型进行trait约束

基础用法

trait约束:指使用泛型时限制泛型类型必须实现哪一些trait。trait约束与泛型类型的参数声明在一起,适用于复杂的开发环境,语法如下:

fn function_name<T: TraitA + TraitB + TraitC>(t: T) {
    // 函数体
}

fn function_name<T: TraitA + TraitB + TraitC>() -> T {
    // 函数体
}

如果泛型参数有多个trait约束,那么拥有多个泛型参数的参数会导致函数名和参数列表之间会有很长的trait约束信息,使得函数签名可读性差,此时可以使用where关键字处理这种情况:

fn function_name<T: TraitA + TraitB + TraitC>(t: T) {}

// 使用where关键字
fn function_name<T>(t: T)
where T: TraitA + TraitB + TraitC {
    
}
    
fn function_name<T>() -> T
where T: TraitA + TraitB + TraitC {
    
}

根据trait约束实现方法

在使用泛型类型参数的impl块上使用约束,可以有条件地为实现了特定trait的类型实现方法。

struct StructName<T> {
    // 结构体实现
}

impl<T: Display + PartialOrd> StructName<T> {
    // 如果T是实现了Display和PartialOrd两种trait的类型,则这里实现的方法会生效
}
  • 也可以为实现了其它trait的任意类型有条件地实现某个trait。为满足trait约束的所有类型上实现trait叫做覆盖实现(blanket implementations)
  • 覆盖实现作用为使具有某项行为act1的类型同时拥有另一项指定行为act2,即只要类型拥有act1,就拥有act2。
  • 覆盖实现与java中的接口继承不一样,覆盖实现需要在trait A实现trait B时同时确保B中的方法已经实现或者立即给出实现,而接口继承则子接口可以不实现父接口的方法,带类实现子接口时再实现。
  • 语法:
    // 实现了TraitName1的类型同时也就实现了TraitName2的方法
    impl<T: TraitName1> TraitName2 for T {
        // 这里可以实现TraitName2中的方法
        // 此时传入的self即有TraitName2的方法也有TraitName1的方法
    }
    
  • 例子:
    // 为实现了Display的类型实现ToString trait
    impl<T: Display> ToString for T {
        // 跳过
    }
    

9.2.3 trait的继承

trait可以被继承,语法如下:

trait B: A

表示trait B继承了A,即在B中能使用A中声明的方法。

9.2.4 Self 类型

  • Self 代表当前的类型,比如 File 类型实现了 Write,那么实现Wirte的逻辑过程中有可能需要用到当前类型File,此时使用到的 Self 就指代 File
  • self 在用作方法的第一个参数时,实际上是 self: Self 的简写,所以 &selfself: &Self, 而 &mut selfself: &mut Self

9.2.5 标准库常用trait

标准库中提供了很多有用的trait,其中一些trait可应用于结构体或枚举定义的derive属性中。对于使用#[derive]语法标记的类型,编译器会自动为其生成对应的trait的默认实现代码。

格式化输出:Debug与Display

Debug trait:

  • 功能描述:开启格式化字符串中的调试格式,常用于调试上下文中以{:?}{:#?}格式打印输出一个类型的实例;
  • 路径:std::fmt::Debug
  • 实现方式:因为都是模式化的代码,所以可以通过#[derive(Debug)] 派生出相关代码;

Display trait:

  • 功能描述:以{}格式化输出一个值的内容,主要用于面向用户的输出,
  • 路径:std::fmt::{Display, Result}
  • 实现方式:实现Displayfmt(&self, f: &mut Formatter<'_>) -> Result 方法;

例子:

use std::fmt::{Display, Formatter, Result};

// Debug的例子
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

// Display的例子
impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "({} {})", self.x, self.y)
    }
}

fn main() {
    let origin = Point {x:0, y:0};
    println!("{}", origin);         // (0,0)
    println!("{:?}", origin);       // Point{ x: 0, y: 0 }
    println!("{:#?}", origin);      // Point {
                                    //  x : 0,    
                                    //  y : 0,
                                    // }
}

等值比较:Eq和PartialEq

Eq与PartialEq的区别:

关系 中文名 对称性(a==b => b==a) 传递性(a==b, b==c => a==c) 反身性(a==a) 例子
Eq 等价关系 整数
PartialEq 局部等价关系 × 浮点数

PartialEq trait:

  • 路径:不需要额外引入,因为就在标准包里面;
  • 实现方式1,实现默认比较逻辑:
    • 在需要实现PartialEq的类型上使用 #[derive(PartialEq)]
    • 只有所有字段的值都相等,比较的两个值才相等,只要有任何字段的值不相等则这两个值不相等;
  • 实现方式2,自定义实现比较逻辑:
    • 类型实现 PartialEqfn eq(&self, other: &Self) -> bool 方法;

Eq trait:

  • 路径:不需要额外引入,因为就在标准包里面;
  • 实现方式:先实现PartialEq,再实现#[derive(Eq)]即可;

例子:

// 实现Eq的例子
#[derive(PartialEq, Eq)]
enum BookFormat {
    Paperback,
    Hardback,
    Ebook,
}

struct Book {
    isbn: i32,
    format: BookFormat,
}

// 实现 PartialEq 的例子
impl PartialEq for Book {
    fn eq(&self, other: &Self) -> bool {
        self.isbn == other.isbn
    }
}

fn main() {
    let b1 = Book { isbn:3, format: BookFormat::Paperback};
    let b2 = Book { isbn:3, format: BookFormat::Ebook};
    let b3 = Book { isbn:5, format: BookFormat::Paperback};
    
    assert!(b1 == b2);
    assert!(b1 != b3);
}

次序比较:Ord与PartialOrd

次序关系说明:

  • 完全反对称性:即任何一对元素之间的关系只能是aa==ba>b中其中一种;
  • 反对称性:若a!(a>b),反之亦然;
  • 传递性:a a==>同理;

Ord与PartialOrd的区别:

关系 中文名 完全反对称性 反对称性 传递性
Ord 全序比较 ×
PartialOrd 偏序比较 ×

Ord trait:

  • 路径:use std::cmp::{Ord, Ordering};Ord会被预导入,所以可以不用手动导入;
  • 实现方式1,默认实现:#[derive(Ord)]
  • 实现方式2,自定义实现:实现Ordfn cmp(&self, other: &Self) -> Ordering; 方法;
  • Ord中有 fn max(self, other: Self) -> Selffn min(self, other: Self) -> Self 等默认实现的方法方便比较操作;

PartialOrd trait:

  • 路径:use std::cmp::{PartialOrd, Ordering};PartialOrd会被预导入,所以可以不用手动导入;
  • 实现方式1,默认实现:#[derive(PartialOrd)]
  • 实现方式2,自定义实现:实现PartialOrdfn partial_cmp(&self, other: &Rhs) -> Option;方法;
  • PartialOrd中有 fn lt(&self, other: &Rhs) -> boolfn le(&self, other: &Rhs) -> boolfn gt(&self, other: &Rhs) -> boolfn ge(&self, other: &Rhs) -> bool 等默认实现的方法方便比较操作;

例子:

use std::cmp::Ordering;

#[derive(PartialEq, Eq)]
struct Person {
    id: u32,
    name: String,
    height: u32,
}

impl Ord for Person {
    fn cmp(&self, other: &Person) -> Ordering {
        self.height.cmp(&other.height)
    }
}

impl PartialOrd for Person {
    fn partial_cmp(&self, other: &Person) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

fn main() {
    let person1 = Person {id:1, name: String::from("zhangsan"), height: 168};
    let person2 = Person {id:2, name: String::from("lisi"), height: 175};
    let person3 = Person {id:3, name: String::from("wangwu"), height: 180};
    
    assert_eq!(person1 < person2, true);
    assert_eq!(person2 > person3, false);
    assert_eq!(person1.lt(&person2), true);
    assert_eq!(person3.gt(&person2), true);
    
    let tallest_person = person1.max(person2).max(person3);
    println!("id: {}, name: {}", tallest_person.id, tallest_person.name);
    
    // id: 3, name: wangwu
    
}

复制值:Clone与Copy

Clone trait:

  • 功能描述:实现Clone可以使类型获得深复制的能力,即对栈上和堆上的数据一起复制;
  • 路径:Clone会被预导入,所以可以不用收到导入;
  • 实现方式1,默认实现:#[derive(Clone)],要求结构体的每个字段或枚举的每一个值都可调用clone方法,意味着所有字段或值的类型都必须实现Clone
  • 实现方式2,自定义实现:实现Clonefn clone(&self) -> Self;方法;

Copy trait:

  • 功能描述:实现Copy的类型具有按位复制其值的能力;
  • 路径:Copy会被预导入,所以可以不用收到导入;
  • Copy继承自Clone,这意味着要实现Copy的类型,必须实现Cloneclone方法。
  • 实现方式1,默认实现:#[derive(Copy)]
  • 实现方式2,自定义实现:实现Clonefn clone(&self) -> Self;方法;

例子:

#[derive(Copy, Clone)]
struct MyStruct {};

// 等价于

struct MyStruct;
impl Copy for MyStruct { }
impl Clone for MyStruct {
    fn clone(&self) -> MyStruct {
        *self
    }
}
  • Rust为数字类型、字符类型、布尔类型、单元值、引用等实现了Copy
  • 对于结构体来说,只有所有字段都实现了Copy,这个结构体才算实现了Copy
  • Copy是一个隐式行为。开发者不能重载Copy行为,它永远是简单的位复制。Copy的隐式行为常发生在执行变量绑定、函数参数传递、函数返回等场景中。
  • 任何类型都可以实现Clone,开发者可以按需实现clone方法。
  • 是否实现Copy可以作为类型是值语义还是引用语义的依据。
  • 结构体类型中只要包含了引用语义的类型字段,就实现不了Copy(会报错)。

默认值:Default

  • 功能描述:为类型提供获取默认值的方法,通常用于为结构体的字段提供默认值。
  • 路径:Default会被预导入,所以可以不用手动导入;
  • 实现方式1,默认实现:#[derive(Default)],要求结构体的每个字段或枚举都实现了Defaultfn default() -> Self;方法;
  • 实现方式2,自定义实现:实现Defaultfn default() -> Self;方法;

例子:

#[derive(Default, Debug)]
struct MyStruct {
    foo: i32,
    bar: f32,
}

// 下面的代码等价于 #[derive(Default)]
/*
impl Default for MyStruct {
	fn default() -> Self {
		MyStruct {foo: 0, bar: 0.0}
	}
}
*/

fn main() {
    let options1: MyStruct = Default::default();
    let options2: = MyStruct {foo: 7, ..Default::default()};
    
    println!("options1: {:?}", options1);   // options1: MyStruct {foo:0, bar:0.0}
    println!("options1: {:?}", options2);   // options2: MyStruct {foo:7, bar:0.0}
}

9.3 生命周期(lifetime)

9.3.1 基础

  • 生命周期:Rust的每一个引用以及包含引用的数据结构,都有一个其保持有效的作用域。生命周期可以视为这个作用域的名字。
  • 语法:以'开头加上小写字母,如'a读作“生命周期a”。生命周期注解位于引用的&操作符之后,并用一个空格将生命周期注解与引用类型分隔开,比如&'a i32
  • 生命周期注解并不改变任何引用的生命周期的大小,只用于描述多个生命周期间的关系(供编译器的借用检查器使用)。
  • 一般情况下编译器可以自动推断出每一个引用的生命周期。当引用的生命周期可能以不同的方式互相关联时,需要手动标注生命周期。

9.3.2 静态生命周期

  • 静态生命周期'static:Rust预定义了一种特殊的生命周期注解'static,它具有和整个程序运行时相同的生命周期。
  • 字符串字面量是直接硬编码到最终的可执行文件中的,因此拥有'static生命周期。
let s: &'static str = "I have a static lifetime.";

9.3.3 生命周期与函数

在函数签名中使用生命周期注解,类似于声明泛型类型的语法:将生命周期声明在函数名和参数列表间的尖括号中。生命周期注解语法用于函数是为了将入参与返回值的生命周期形成某种关联,这样Rust就有足够的信息来保证内存安全,以防止产生悬垂引用等不安全的行为。

fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

9.3.4 生命周期与结构体

  • 有的时候结构体的成员可能是一个引用类型的字段,这个时候需要在结构体定义中使用生命周期注解,类似于声明泛型类型:将生命周期声明在结构体名称构面的尖括号中
  • 要为带有生命周期的结构体实现方法,需要在impl关键字后面的尖括号中声明生命周期。
  • 一般不需要在方法签名中使用生命周期注解,除非结构体字段、方法参数以及返回值的生命周期需要相关联。

例子:

struct Foo<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

impl<'a, 'b> Foo<'a, 'b> {
    fn get_x(&self) -> &i32 { self.x }
    fn get_y(&self) -> &i32 { self.y }
    fn max_x(&'a self, f: &'a Foo) -> &'a i32 {
        if self.x > f.x {
            self.x
        } else {
            f.x
        }
    }
}

fn main() {
    let f1 = Foo {x: &3, y: &5};
    let f2 = Foo {x: &7, y: &9};
    println!("x: {}, y: {}", f1.get_x(), f1.get_y());
    println!("max_x: {}", f1.max_x(&f2));
}

// x: 3, y: 5
// max_x: 7

9.3.5 (重要)生命周期省略规则

  • 这些规则适用于函数或方法定义。
  • 函数或方法参数的生命周期称为输入生命周期,返回值的生命周期称为输出生命周期。
  1. 每一个被忽略生命周期注解的参数,都具有各不相同的生命周期。
    • fn foo(x: &i32, y: &i32)等价于fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
  2. 如果只有一个输入生命周期(无论是否省略),这个生命周期会赋给所有被省略的输出生命周期。
    • fn foo(x: &i32) -> &i32等价于fn foo<'a>(x: &'a i32) -> &'a i32
  3. 方法中self的生命周期会赋给所有被省略的输出生命周期。
  • 三条生命周期的省略规则:
    • 第一条适用于输入生命周期。
    • 第二条适用于输出生命周期。
    • 第三条适用于方法前面。

你可能感兴趣的:(第N次入门Rust,rust,开发语言,后端)