温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

rust生命周期源码分析

发布时间:2023-03-17 11:22:12 来源:亿速云 阅读:100 作者:iii 栏目:开发技术

本文小编为大家详细介绍“rust生命周期源码分析”,内容详细,步骤清晰,细节处理妥当,希望这篇“rust生命周期源码分析”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

    rust生命周期

    生命周期是rust中用来规定引用的有效作用域。在大多数时候,无需手动声明,因为编译器能够自动推导。当编译器无法自动推导出生命周期的时候,就需要我们手动标明生命周期。生命周期主要是为了避免悬垂引用

    借用检查

    rust的编译器会使用借用检查器来检查我们程序的借用正确性。例如:

    #![allow(unused)]
    fn main() {
    {
        let r;
    
        {
            let x = 5;
            r = &x;
        }
    
        println!("r: {}", r);
    }
    }

    在编译期,Rust 会比较两个变量的生命周期,结果发现 r 明明拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。

    函数中的生命周期

    #![allow(unused)]
    fn main() {
    fn longest(x: &str, y: &str) -> &str {
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }
    }

    执行这段代码,rust编译器会报错,它给出的help信息如下:

    help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

    意思是函数返回类型是一个借用值,但是无法从函数的签名中得知返回值是从x还是y借用的。并且给出了相应的修复代码。

    4 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
      |           ++++     ++          ++          ++

    按照这个提示,我们更改函数声明。就会发现可以顺利通过编译。因此,像这样的函数,我们无法判断它是返回x还是y,那么只好手动进行生命周期声明。上面的提示就是手动声明声明周期的语法。

    手动声明生命周期

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

    生命周期的语法是以&rsquo;开头,名称往往是一个单独的小写字母。大多数人用&rsquo;a来作为生命周期的名称。如果是引用类型的参数,生命周期会位于&之后,并用空格来将生命周期和参数分隔开。函数签名中的生命周期标注和泛型一样,需要在提前声明生命周期。例如我们刚才修改过的函数签名

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

    该函数签名表明对于某些生命周期 'a,函数的两个参数都至少跟 'a 活得一样久,同时函数的返回引用也至少跟 'a 活得一样久。实际上,这意味着返回值的生命周期与参数生命周期中的较小值一致:虽然两个参数的生命周期都是标注了 'a,但是实际上这两个参数的真实生命周期可能是不一样的(生命周期 'a 不代表生命周期等于 'a,而是大于等于 'a)。例如:

    fn main() {
        let string1 = String::from("long string is long");
    
        {
            let string2 = String::from("xyz");
            let result = longest(string1.as_str(), string2.as_str());
            println!("The longest string is {}", result);
        }
    }

    result 的生命周期等于参数中生命周期最小的,因此要等于 string2 的生命周期,也就是说,result 要活得和 string2 一样久。如过我们将上面的代码改变为如下所示。

    fn main() {
        let string1 = String::from("long string is long");
        let result;
        {
            let string2 = String::from("xyz");
            result = longest(string1.as_str(), string2.as_str());
        }
        println!("The longest string is {}", result);
    }

    那么将会导致错误,因为编译器知道string2活不到最后一行打印。而string1可以活到打印,但是编译器并不知道longest返回的是谁。
    函数的返回值如果是一个引用类型,那么它的生命周期只会来源于:

    • 函数参数的生命周期

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

    若是后者情况,就是典型的悬垂引用场景:

    #![allow(unused)]
    fn main() {
    fn longest<'a>(x: &str, y: &str) -> &'a str {
        let result = String::from("really long string");
        result.as_str()
    }
    }

    上面的函数的返回值就和参数 x,y 没有任何关系,而是引用了函数体内创建的字符串,而函数结束的时候会自动释放result的内存,从而导致悬垂指针。这种情况,最好的办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者:

    fn longest<'a>(_x: &str, _y: &str) -> String {
        String::from("really long string")
    }
    
    fn main() {
       let s = longest("not", "important");
    }

    结构体中的生命周期

    在结构体中使用引用,只要为结构体中的每一个引用标注上生命周期即可。

    struct ImportantExcerpt<'a> {
        part: &'a str,
    }
    
    fn main() {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let first_sentence = novel.split('.').next().expect("Could not find a '.'");
        let i = ImportantExcerpt {
            part: first_sentence,
        };
    }

    part引用的first_sentence来自于novel,它的生命周期是main函数,因此这段代码可以正常工作。
    ImportantExcerpt 结构体中有一个引用类型的字段 part,因此需要为它标注上生命周期。结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>。该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。

    生命周期消除

    编译器为了简化用户的使用,运用了生命周期消除大法。例如:

    fn first_word(s: &str) -> &str {
        let bytes = s.as_bytes();
    
        for (i, &item) in bytes.iter().enumerate() {
            if item == b' ' {
                return &s[0..i];
            }
        }
    
        &s[..]
    }

    对于 first_word 函数,它的返回值是一个引用类型,那么该引用只有两种情况:

    • 从参数获取

    • 从函数体内部新创建的变量获取

    如果是后者,就会出现悬垂引用,最终被编译器拒绝,因此只剩一种情况:返回值的引用是获取自参数,这就意味着参数和返回值的生命周期是一样的。道理很简单,我们能看出来,编译器自然也能看出来,因此,就算我们不标注生命周期,也不会产生歧义。

    只不过,消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
    函数或者方法中,参数的生命周期被称为 输入生命周期,返回值的生命周期被称为 输出生命周期

    三条消除原则

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

    例如一个引用参数的函数就有一个生命周期标注: fn foo<'a>(x: &'a i32),两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32), 依此类推。

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

    例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<'a>(x: &'a i32) -> &'a i32

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

    拥有 &self 形式的参数,说明该函数是一个 方法,该规则让方法的使用便利度大幅提升。

    让我们假装自己是编译器,然后看下以下的函数该如何应用这些规则:

    例子1

    fn first_word(s: &str) -> &str // 实际项目中的手写代码

    首先,我们手写的代码如上所示时,编译器会先应用第一条规则,为每个参数标注一个生命周期:

    fn first_word<'a>(s: &'a str) -> &str  // 编译器自动为参数添加生命周期

    此时,第二条规则就可以进行应用,因为函数只有一个输入生命周期,因此该生命周期会被赋予所有的输出生命周期:

    fn first_word<'a>(s: &'a str) -> &'a str  // 编译器自动为返回值添加生命周期

    此时,编译器为函数签名中的所有引用都自动添加了具体的生命周期,因此编译通过,且用户无需手动去标注生命周期,只要按照 fn first_word(s: &str) -> &str的形式写代码即可。

    例子2

    fn longest(x: &str, y: &str) -> &str // 实际项目中的手写代码

    首先,编译器会应用第一条规则,为每个参数都标注生命周期:

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

    但是此时,第二条规则却无法被使用,因为输入生命周期有两个,第三条规则也不符合,因为它是函数,不是方法,因此没有 &self 参数。在套用所有规则后,编译器依然无法为返回值标注合适的生命周期,因此,编译器就会报错,提示我们需要手动标注生命周期。

    例子3

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

    首先,编译器应用第一规则,给予每个输入参数一个生命周期。

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

    需要注意的是,编译器不知道 announcement 的生命周期到底多长,因此它无法简单的给予它生命周期 'a,而是重新声明了一个全新的生命周期 'b。接着,编译器应用第三规则,将 &self 的生命周期赋给返回值 &str

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

    尽管我们没有给方法标注生命周期,但是在第一和第三规则的配合下,编译器依然完美的为我们亮起了绿灯。

    生命周期约束

    我们来看下面这个例子。将返回值的生命周期声明为&rsquo;b,但是实际返回的是生命周期为&rsquo;a的self.part。

    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 活得久

    • 可以把 'a 和 'b 都在同一个地方声明(如上),或者分开声明但通过 where '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
        }
    }

    加上这个约束,告诉编译器&rsquo;a活的比&rsquo;b更久,引用&rsquo;a不会产生悬垂指针(无效引用)。

    静态生命周期

    rust中有一个非常特殊的生命周期,那就是&rsquo;static,拥有该生命周期的引用可以活的和整个程序一样久。实际上字符串字面值就拥有&rsquo;static生命周期,它被硬编码进rust的二进制文件中。'static生命周期非常强大,随意使用它相当于放弃了生命周期检查。遇到因为生命周期导致的编译不通过问题,首先想的应该是:是否是我们试图创建一个悬垂引用,或者是试图匹配不一致的生命周期,而不是简单粗暴的用 'static 来解决问题。除非实在遇到解决不了的生命周期标注问题,可以尝试&rsquo;static生命周期。例如:

    fn t() -> &'static str{
        "qwert"
    }
    
    fn t() -> &'static str{
        "qwert"
    }

    注意,使用&rsquo;static生命周期的时候,不需要提前声明。

    一个复杂例子: 泛型,特征约束以及生命周期

    use std::fmt::Display;
    
    fn longest_with_an_announcement<'a, T>(
        x: &'a str,
        y: &'a str,
        ann: T,
    ) -> &'a str
    where
        T: Display,
    {
        println!("Announcement! {}", ann);
        if x.len() > y.len() {
            x
        } else {
            y
        }
    }

    例子中,包含了生命周期&rsquo;a,泛型T以及对T的约束Display(因为我们需要打印ann)。

    读到这里,这篇“rust生命周期源码分析”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI