Day 07 - 泛型 特性 生命周期

本贴最后更新于 390 天前,其中的信息可能已经水流花落

image

img

📚 泛型

创建时间:2023-04-02 02:40 星期天


抽象的解释:泛型是具体类型或其他属性的抽象代替

你编写了一个函数但是你不确定他的返回值或者参数值的类型是什么,你就可以用泛型来作为模板

fn largest<T>(list: &[T]) -> T{}

这就实现了我们所谓的单态化, 比如我们找最大值的方法:

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

fn main() {
    let int_queue = [1, 5, 3, 5, 6, 8, 4];
    let char_quque = ['a', 'b', 'd', 'c', 'w', 's'];

    let max_int = largest(&int_queue);
    let max_char = largest(&char_quque);
}

因为 max_char​ 是 char 的数组所以他的引用无法传入 largest, 为了让 int_queue​ 和 max_char​ 都可以传入方法我们可以

函数中的泛型

以下的代码还是有问题 但是涉及太深奥的知识我们之后再将 这里只是讲解泛型如何使用:

fn largest<T: std::cmp::PartialOrd> (list: &[T]) -> T{
    let mut max = list[0];
    for &i in list{
        if i > max{
            max = i;
        }
    };
    max
}

fn main() {
    let int_queue = [1, 5, 3, 5, 6, 8, 4];
    let char_quque = ['a', 'b', 'd', 'c', 'w', 's'];

    let max_int = largest(&int_queue);
    let max_char = largest(&char_quque);
}

核心代码就是 fn largest<T: std::cmp::PartialOrd> (list: &[T]) -> T​ :

  • <T: std::cmp::PartialOrd>​ 表示我需要在这个函数中申明一个泛型 T 它实现了比较的功能
  • list: &[T]​ 表示我的参数是一个 T 类型组成的列表
  • -> T​ 返回值是 T 类型的

结构体中的泛型

struct Point<T>{
    x: T,
    y: T
}

fn main() {
    let point1 = Point { x:4, y:4 };
    let point2 = Point { x:5.2, y:2.4 };
}

这样我们就可以同时利用一个结构体创建不同类型的了!

如果你想 x y 是不同类型的你还可以

struct Point<T,S>{
    x: T,
    y: S
}

fn main() {
    let point1 = Point { x:4, y:4 };
    let point2 = Point{x:5.2, y:2.4};
}

枚举的泛型

我们之前的 Result<T,E>​ 就是一个很好的例子

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

方法 impl 中的泛型

struct Point<T,S>{
    x: T,
    y: S
}

impl <T,S> Point<T,S> {

    fn x(&self) -> &T {
        &self.x
    }
  
}

fn main() {
    let point1 = Point { x:4, y:4 };
    let point2 = Point{x:5.2, y:2.4};
}

当我们指定了类型实现了方法的时候其他类型的变量将无法引用其方法

struct Point<T,S>{
    x: T,
    y: S
}

impl <T,S> Point<T,S> {

    fn x(&self) -> &T {
        &self.x
    }
  
}
impl  Point<i32,i32>{

    fn x_int(&mut self) -> i32{
        self.x += 1;
        self.x
    }
  
}
fn main() {
    let mut point1 = Point { x:4, y:4 };
    let point2 = Point{x:5.2, y:2.4};
    point1.x_int();
    point2.x(); // point 没有 x_int
    print!("{:?}", point1.x);
}

补充:

Struct 的泛型类型可以和方法的泛型类型参数不同

​​image​​

单态化

编译的时候将不同类型的变量转换成具体类的过程

比如我们的 Options 的 Some

let i = Some(5);
let j = Some(5.1);

别看他们都是 Some 因为泛型不同他们会走不同的枚举在编译期间 Some 会根据泛型的不同生成 Some_i32​之类的数据

所以上述的源码是:

let  i  = Some_i32(5);
let  j  = Some_f64(5.1);

image

img

📚 Trait

创建时间:2023-04-02 04:25 星期天


trait 相当于 interface虚函数

简单的定义

pub trait fly_able {
    fn fly(&self) -> i32; // 返回当前飞行高度
}

接着是我们的实现

pub struct brid {
    pub name: String,
    pub _where: String,
}

pub trait fly_able {
    fn fly(&self) -> i32; // 返回当前飞行高度
}

impl fly_able for brid {
    // 让鸟实现飞翔的能力
    fn fly(&self) -> i32 {
        println!("{} is  Flying", &self.name);
        return 100;
    }
}

我们把代码扔在 lib.rs 中

然后我们在 main.rs 中写入:

use rustDay01::{brid, fly_able};

fn main(){
    let laoying = brid{
        name: "老鹰".to_string(),
        _where: "天空上".to_string()
    };

    laoying.fly();
}

rustDay01 是 Cargo.toml 中 [package] 的 name 值

孤儿法则,避免使用第三方 trait 去 impl 第三方 struct,破坏代码结构

默认实现

相当于 java 的 defalut 函数

pub trait fly_able {
    fn fly(&self) -> i32{
        panic!("请实现我这个无助的代码吧");
    } // 返回当前飞行高度
}

impl fly_able for brid {
    // 让鸟实现飞翔的能力
  
}

而且默认实现的方法也可以去调用那些抽象的方法,但是无法在重写方法中调用默认的实现

在函数/方法中使用 trait

如果某个函数的参数是实现 trait 的类型,我们可以这么写:

use rustDay01::{brid, fly_able};

fn fly_to_2m(_brid:impl fly_able){
    _brid.fly();
}
fn main(){
    let laoying = brid{
        name: "老鹰".to_string(),
        _where: "天空上".to_string()
    };

    fly_to_2m(laoying);
}

我们也可以使用泛型进行约束

fn fly_to_2m<T: fly_able>(_brid:T){
    _brid.fly();
}

// main 方法同上

如果是需要参数实现 多个trait​的话 可以这么写:

fn fly_to_2m<T: fly_able + run_able>(_brid:T){
    _brid.fly();
}
// 或者 
fn fly_to_2m(_brid:impl fly_able + run_able){
    _brid.fly();
}

当然我们这么写可能不直观我们可以

fn fly_to_2m_new<T, B> (_brid: T, _brid2: B) -> T 
where 
    T: fly_able + run_able,
    B: fly_able 
{
    _brid
}

注意 当我们指定了某个 triat 类型作为返回值的时候 函数只能返回同一种类型的 比如 T = fly_able​ 我有结构 Brid​ 和 Plane​ 的类型都满足 T​,但是函数一旦返回了 Brid​ 就无法返回 Plane​ 了

我们之前找最大值的代码现在就可以完成了

fn largest<T: std::cmp::PartialOrd + Copy> (list: &[T]) -> T{
    let mut max = list[0];
    for &i in list{
        if i > max{
            max = i;
        }
    };
    max
}

fn main() {
    let int_queue = [1, 5, 3, 5, 6, 8, 4];
    let char_quque = ['a', 'b', 'd', 'c', 'w', 's'];

    let max_int = largest(&int_queue);
    let max_char = largest(&char_quque);

    println!("{}", max_char);
    println!("{}", max_int);
}

Why :

因为比大小 也就是 > < 操作我们需要 两个元的变量都实现 T: std::cmp::PartialOrd​ , 又因为 字符 和 i32 这些类型是栈存储的 每次赋值的时候不是移动所有权而是复制 针对复制的我们需要 T 也有 Copy 的实现

img

📚 ★ 生命周期

创建时间:2023-04-02 16:25 星期天


Rust 的每个引用都有自己的生命周期(保持有效的作用空间) 生命周期大多情况下是隐式存在的 可被推断出来的 当引用的生命周期可能以不同方式互相关联的时候需要手动标注生命周期

为什么又生命周期: 因为避免引用 出现 悬垂引用

悬垂引用: 在 Rust 中,悬垂引用是指一个指针(我们可以暂时理解为类似引用的东西)指向了已经被释放的内存,这种情况会导致程序崩溃或者产生不可预期的行为。为了避免悬垂引用的问题,Rust 引入了生命周期的概念,用来描述引用的有效范围。

我们先看看以下这个代码:

这段代码是有错误的 因为我们在最后打印 r 的值的时候 我们的 r 所指向的 x 已经离开了属于他的作用域 也就是你引用了一个已经被销毁的变量

fn main(){
     let r;
     {
        let x = 5;
        r = &x;  
     }
     println!("{}",r );
}

借用检查器:Rust 编译器的借用检查器的机制就是 比较作用域来判断所有的借用是否合法,我们知道上述代码中 x 的生命周期比 r 短 。所以当我们把赋值一个变量的时候 借用检查器用判断被赋值的变量的生命周期是否比赋值的值(右边的值)短,否则会发生上例的错误。

泛型的生命周期

fn main(){
    let string1 = "你好我是刘xx".to_string();
    let string2 = String::from("谁是我的喜欢秀");
}

fn comper(x : &str, y: &str) -> &str{
    if x.len() > y.len(){
        x // 'b
    }else{
        y // ‘a
    }
}


我们函数的签名存在一个返回值但是我们的返回值因为没有申明生命周期 函数在后期的时候 但是会出先一个缺少生命周期的标注 因为我们上面这个函数要么返回 x 要么返回 y 此时借用检查器就无法清晰的知道我们的 xy 那个生命周期死亡哪个还活着了。

当一个函数返回类型包含一个借用值的时候函数的签名而且这个函数的返回值可能会出现多个不同的引用,也就是体现不出来返回的是哪一个参数的时候,我们就需要给这个函数的签名加上一个生命周期参数了。

fn comper(x : &str, y: &str) -> &str{
    x // 也会报错 因为体现不出来是返回的 x 还是 y
}

所以我们需要利用泛型申明一个生命周期

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

这里我们可以先理解为我们定义了一个生命周期 a 使传入的参数 x y 都属于生命周期 a 且返回的值的生命周期也是生命周期 a。

生命周期标注语法

生命周期不会延长或者缩短引用的生命周期的长度,当指定了泛型生命周期的时候函数可以接受带有任何生命周期的引用。生命周期标注只是描述了参数之间生命周期的关系而不会改变他们生命周期。

在 Rust 中,字符串字面量是一个静态分配的不可变引用,它的生命周期是 'static​。这意味着它们在程序的整个生命周期内都是有效的,因此可以在任何地方使用。

当你使用 let a = "asd";​ 时,编译器会自动为 a​ 推断出类型为 &'static str​,因为字符串字面量的生命周期是 'static​。这意味着 a​ 的生命周期与程序的整个生命周期相同,因此在全局有效。

如果你想创建一个具有不同生命周期的字符串引用,可以使用 String​ 类型,它是动态分配的,并且可以在运行时创建和销毁。例如,let a = String::from("asd");​ 将创建一个具有动态生命周期的字符串引用。

制裁他的只有所有权~

函数的生命周期相当于结盟之前的口号:不求同年生但求同年死。但是我们不能修改他们长寿和短命的生命基因。(短命的依旧短命 长寿的依旧长寿 )但是结盟这种事件可以导致我们的生命周期的描述(只是字面描述)是一致的

我们可以看这几段代码

fn comper<'a>(x : &'a str, y: &'a str) -> &'a str{
    if x.len() > y.len(){
        x
    }else{
        y
    }
} // 这是之后我们都要用的函数
fn main(){
    let string1 = "你是我的我是你的谁".to_string();
    let string3;
    let string2 = "再多看一眼就会爆炸".to_string();

    {
        string3 = comper(string1.as_str(), string2.as_str());
    }
    println!("{}", string3); // SIGN A
}

因为 string3 的生命周期在 SIGN A​ 的时候依旧存活 所以代码有效

​​

fn main(){
    let string1 = "你是我的我是你的谁".to_string();
    let string3;

    {
        let string2 = "再多看一眼就会爆炸".to_string();
        string3 = comper(string1.as_str(), string2.as_str());
    } // SIGN B
    println!("{}", string3);
}

上述代码我们把 string2​从外部 {}​ 移动到了 内部的 {}​ 也就是说一旦 string2​ 离开了 SIGN B​ 生命周期就会结束。

函数 comper 的 'a 表示传入参数的两个变量中最短的那个生命周期 string1​ 是外部 {}​的, string2​是内部 {}​. 所以 'a​就是 string2​的生命周期返回的也是一个与 string2​生命周期一致 但是我们打印的时候 string2​已经死了所以返回值也死了 导致 string3 死了 所以最后打印程序报错(程序也死了​).

也就是我们可以说我们在方法签名定义的时候定义的'a,它的生命周期取决于该方法的参数中(这个参数需要'a 进行标识)最短命的那个生命周期并将这个生命周期交给我们的返回值(如果返回值有生命周期的话)。

深入生命周期

我们可以给需要的参数附上生命周期不需要的可以不用。

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

因为我们只需要 x 所以没必要给 y 也加上参数。

现在我们需要注意当函数返回一个引用的时候记得这个返回的引用的生命周期要与参数的生命周期进行匹配

fn comper<'a>(x : &'a str, y: & str) -> &'a str{
    let c = format!("{x}-{y}");
    &c
} 

上面的代码是错误的, 因为我们在函数内部创建的值只能在函数内部有效他的生命周期就这么一点 离开函数,c 指向的内存就被清洗了

fn comper<'a>(x : &'a str, y: & str) -> String {
    let c = format!("{x}-{y}");
    c
} 

所以我们直接返回值

在结构中使用引用

在结构中使用引用我们需要使用标注

struct User<'a>{
    username : &'a str
}

fn main() {
    let string1 = String::from("你好/啊/我是/刘");
    let string2 = string1.split('/').next().expect("找不到`.`!"); // 通过切割找到第一个
    let guest = User{
        username: string2
    };
}

如果结构体存在引用'a 那么需要保证'a 的变量需要较结构的生命周期是不低于的.

生命周期的省略

我们在 02 的切片教程中有过这么一段代码

fn main() {
    let mut a = String::from("Hell o World");
    let ret = {
        let b = get_char(&mut a); // 我们在这里拿到了一个 可写切片的引用
        println!("{}", b);
        b // 我们这里单纯的将切片交给外部
    };
    a.push_str("123"); // 这里我们进行另外一个可写变量的使用
}

fn get_char(s: &mut str) -> &mut str {
    let first_word_len = s.find(' ').unwrap_or_else(|| s.len());
    let first_word = &mut s[..first_word_len];
    first_word
}

image

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

输出生命周期:返回值。

目前的编译器使用三个规则来进行生命周期的省略

  • 适用于输入生命周期

    • 每个引用类型参数都有自己的生命周期;
    • 只有一个输入生命周期参数的时候, 他会把这个输入生命周期的生命周期交给返回值
  • 适用于输出生命周期

    • 当参数存在&self,返回类型就是&self 的返回生命周期

当这些规则一个一个执行完毕后 方法的签名任然后成员不存在生命周期的时候 编译器报错

fn(x:&, y:&) -> &;​ -- 符合规则 1 --> fn<'a,'b>(x:&'a, y:&'b) -> &​ --不符合规则 2--> 不变​ --不符合规则 3 --> 不变

所以 fn(x:&, y:&) -> &;​ == fn<'a,'b>(x:&'a, y:&'b) -> &

因为 fn<'a,'b>(x:&'a, y:&'b) -> &​ 不符合 所有参数和返回值都有生命周期 所以报错;


fn(x:&) -> &;​ -1> fn<'a>(x:&'a) -> &;​ -2> fn<'a>(x:&'a) -> &‘a;​ -3:不符合 > xxx -> fn<'a>(x:&'a) -> &'a;

因为 fn<'a>(x:&'a) -> &'a;​ 符合 所有参数和返回值都有生命周期 所以不报错;

方法的生命周期

在 struct 上使用生命周期实现方法语法和泛型参数一样。

在哪里声明和使用生命周期,依赖于

  • 生命周期参数是否和字段、方法参数或者返回值有关。
struct User<'a>{
    username : &'a str
}

impl<'b> User<'b>{
    fn level(&self) -> i32{
        4 
    }
}
impl<'b> User<'b>{
    fn level(&self) -> i32{
        4 
    }

    fn level_str(&self, x: &'b str, y: &'b str) -> &str{
        x
    }
}

'static 生命周期

'static 生命周期是一个比较特殊的生命周期 整个程序的生命周期就是他的生命周期

  • Rust

    Rust 是一门赋予每个人构建可靠且高效软件能力的语言。Rust 由 Mozilla 开发,最早发布于 2014 年 9 月。

    57 引用 • 22 回帖 • 5 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...