Rust快速入门(五)

发布于:2024-12-18 ⋅ 阅读:(13) ⋅ 点赞:(0)

生命周期

生命周期的主要作用是避免悬垂引用。

这里我们详细说说借用检查:

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

rust使用了一个借用检查器来检查程序正确性,它会比较两个变量声明周期,比如上述代码中:r的生命周期是 'a ,x的生命周期是'b,由于a引用了b,由于b远远小于a,所以编译器会拒绝执行上述代码。

函数中的生命周期

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";
​
    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
​
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

上述代码在rust中会报错,因为在longest函数中,编译器无法知道该函数的返回值到底引用 x 还是 y

也可以手动定义两个生命周期的长度规范:

impl<'a: 'b, 'b> ImportantExcerpt<'a> {
    fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

上述<>内容表示 :

  • 'a 的生命周期至少和 'b 一样长

  • 'a 必须比 'b 活得更久或同样长

也可以这样做:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
    where
        'a: 'b,
    {
        println!("Attention please: {}", announcement);
        self.part
    }
}

所以这里引出一个概念:在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。

生命周期标注语法

生命周期标注并不会改变任何引用的实际作用域。标记生命周期只是为了取悦编译器,不让编译器为难我们。

生命周期的语法以' 开头:

&i32 // 普通引用
&'a i32 // 具有显示生命周期的引用
&'a mut i32 // 具有显示生命周期的可变引用

声明的规则:

  • 和泛型一样,使用生命周期参数,需要先声明 <'a>

  • xy 和返回值至少活得和 'a 一样久(因为返回值要么是 x,要么是 y

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

举一个例子:

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

上述代码中,返回值的生命周期与参数生命周期中的较小值一致,虽然两个参数的生命周期都是标注了 'a,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a 不代表生命周期等于 'a,而是大于等于 'a)。

函数的返回值如果是一个引用类型,那么它的生命周期只会来源于

  • 函数参数的生命周期

  • 函数体中某个新建引用的生命周期

结构体中的生命周期

与函数类似,不过多赘述

生命周期消除

对于编译器来说,每一个引用类型都有一个生命周期,编译器为了简化用户的使用,运用了生命周期消除大法

注意:

  • 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期

  • 函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期

三条消除规则:

  • 每一个引用参数都会获得独自的生命周期

  • 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

  • 若存在多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期被赋给所有的输出生命周期

静态生命周期

'static,拥有该生命周期的引用可以和整个程序活得一样久。

返回值与错误处理

panic

panic!()

panic时两种终止方式:栈展开直接终止

默认的方式就是 栈展开,这意味着 Rust 会回溯栈上数据和函数调用,因此也意味着更多的善后工作,好处是可以给出充分的报错信息和栈调用信息,便于事后的问题复盘。直接终止,顾名思义,不清理数据就直接退出程序,善后工作交与操作系统来负责。

线程panic如果不是main线程,那么程序就不会终止。

可恢复的错误Result

使用枚举Result:

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

实际例子:

use std::fs::File;
​
fn main() {
    let f = File::open("hello.txt");
​
    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("Problem opening the file: {:?}", error)
        },
    };
}

失败直接崩溃的 panic: unwrap 和 expect

如果返回成功,就将 Ok(T) 中的值取出来,如果失败,就直接 panic

use std::fs::File;
​
fn main() {
    let f = File::open("hello.txt").unwrap();
}
use std::fs::File;
​
fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

except相当于重载错误打印函数。

传播错误宏:?

use std::fs::File;
use std::io;
use std::io::Read;
​
fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

它的作用跟 match 几乎一模一样