rust学习十一.3、生命周期标记

生命周期,这是在"引用和借用“章节就提到的概念,大意是每个变量具有其作用域范围。

所以,我个人更愿意理解为作用范围。 因为它不像java的变量那样和时间有较为明显的关联,毕竟java的变量会被GC销毁。

一、 生命周期注解概念引入

 在原文中,作者是通过两个例子解释生命周期问题

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

 

作者给的图很清楚地展示了作用范围所导致的生命周期问题(作用范围问题)。

对于上图的第一种情况,在没有特殊处理的情况下,r是借不到到x的值(离开返回后x就会被销毁了)

 

另外一个例子

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

 

这个编译会发生错误,作者给出的大体解释是这样:编译器无法知道返回的是哪一个变量的引用,既然无法知道是哪一个,

那么就无法知道其作用范围,所以会报告错误(总之,道理都是rust说的)。为了避免这种情况,rust通过编译器提示我们需要添加符号来表示:无论哪一个参数都是一样的作用范围。

这个符号就是一种类似泛型的写法

例如上面的函数应该修改为:

 fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str {
     if str1.len() > str2.len() {
        str1
     } else {
        str2
     }
 }

 

这个符号还是挺怪异的:fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str

用<'a>表示这是生命周期说明,而不是一般的通用类型说明(通用类型是<T>)。

只有所有的参数和返回都要带上‘a,表示它们的周期都是'a,都是一样的。

完整代码

 fn largest_str<'a>(str1: &'a str, str2: &'a str) -> &'a str {
     if str1.len() > str2.len() {
        str1
     } else {
        str2
     }
 }

 fn main() {
    let s1="good";
    let s2="bad";
    let result=largest_str(s1, s2);
    println!("{}和{}种较长的一个是{}", s1,s2,result);
 }

 

注解符号

  • 必须用单引号(')开头
  • 单引号后面通常跟上小写字母
  • <'a>,使用尖括号包括‘a,表示引入生命周期符号'a

注意事项

  • 注解符号作用在单个方法参数上是没有意义(意思是:如果只是单个,那么可以不加,因为rustc能够识别出来)
  • 在单个方法参数情况下无意义,但不表示其它情况下不需要
  • 只有借助于编译器,我们才会逐渐明白:哪里需要添加生命周期注解符号

二、让人迷惑的生命周期

2.1哪里要引入生命周期符号

除了前面方法/函数中多个引入参数的情况需要说明,还有说明情况需要说明了?

rust发明人定义了其内核,所以哪里需要书写生命周期符号,并不是那么明确。

#[derive(Debug)]
struct student<'a>{
    name: &'a str,
    age: i32,
}
enum cars<'a>{
    byd(&'a str),
}
fn print_str(s:&str)->&str{
    s
}

fn main() {
    let s = student {
        name: "Tom",
        age: 18,
    };
    println!("{:?}",s); 
    let s1 = print_str(s.name);
}

例如上例,如果结构和枚举没有引入生命周期符号,那么编译都会错误,而函数print_str并不会错误。

对于函数print_str用书上给出的理由可以说得通,那么结构中又是什么意思了? 也许需要查看编译器代码才知道为什么。

但对外就不那么美好多了,好在编译器足够体贴。

2.2 是不是提示有问题?

这里直接给出就是书上的例子:

 fn main() {
    let s1="good";
    let s2="bad";
    let result=largest_str(s1, s2);
    println!("{}和{}种较长的一个是{}", s1,s2,result);        

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

 

这里编译报错如下:

这个会报错,利用前面所有权的知识,就能知道为什么,所以这应该是所有权的问题,而不应该提示“borrowed value does not live long enough”.

当出现让人迷糊的提示的时候,应该优先考虑是否所有权出现问题,而不是所谓的其他的生命周期。

 

上面的例子中,如果删除最有一句那么是不会报错的:result = largest_str(string1.as_str(), string2.as_str()); 

所以,逻辑上,这里调用larget_str不是问题,而是对result超出范围使用,因为result无法访问string2.

三、深入理解生命周期

即讨论以下几个问题:

  1. 不同程序结构中的生命周期定义
  2. 如何判断是否需要定义申明周期定义
  3. 可以省略的注解
  4. 静态生命周期

部分问题,前面已经讨论过。

1、2、3问题其实都可以归纳为一个:到底什么情况下需要引入生命周期注解符号?

给出一个可以将就的答案:等编译器提示的时候再录入不迟。

3.1不同程序结构中的生命周期定义

在以下几中程序结构中,都可能需要引入:

  1. 结构-如果有引用,则必须有
  2. 枚举-如果有引用,则必须有
  3. 方法/函数-需要看情况。一般如果只有一个参数可以考虑不要;或者如果能够明确知道总是和某一个参数相关也可以不要

部分在前面的例子中,已经给出结论,此处示例略。

3.2省略的生命周期注解

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[..]
}

上例中,本来是需要注解,但是rust团队后来发现遵循一些特定规则的,可以不要,以为可以推定出来。

还有例如:

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

这是因为编译器可以推定返回的只和x有关。 这是属于有多个引用参数且返回引用类型的。

还是这个例子。虽然可以通过编译器推定出返回结果只和第一个参数有关,但是依然需要为第一个参数添加生命周期注解。这颇为迷惑,如果我们再看看下面这个代码:

fn f_beat_rustee(content: &str)->&str{
    content
}

 

这个是不需要的。

 

对于程序员而言,如何比较简单地判断:

  1. 如果有多个引用参数且返回也是引用,那么一般是需要的注解的
  2. 如果只有一个引用参数,通常可以不要

如果实在还不行,就等着编译器提示吧。

 

3.3 规则

函数或方法的参数的生命周期被称为 输入生命周期input lifetimes),而返回值的生命周期被称为 输出生命周期output lifetimes)。

编译器采用三条规则来判断引用何时不需要明确的注解。

第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块。

注意:这个规则只适用于函数/方法

 

第一条规则(输入、即用于参数)

编译器为每一个引用参数都分配一个生命周期参数。换句话说就是,函数有一个引用参数的就有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

 

第二条规则(输出、用于返回)

如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

 

第三条规则(输出、用于返回)

如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

3.4静态生命周期

'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期

let s: &'static str = "I have a static lifetime.";
作者提醒我们:应该谨慎使用,毕竟这些会在整个程序运行期间占用内容。

 

通常而言,只要不是过分,也不应过于担心。看看java写的后台代码,那是一坨坨,一堆堆,一簇簇...的静态常量。

但java是不用关心性能(相对而言)。

对于rust中,该用还是用,不过分以至于影响性能即可。

 

3.5 同时引入生命周期标记和通用类型

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

 这个例子主要告诉我们:如何引入

 

四、小结

  1. 为什么需要生命周期注解?主要原因是因为编译器无法推断出变量的作用范围,必须人工告知它们的作用范围是一致的。如果你不告诉它,让它在运行时处理,那么大费心机弄得所有权和生命周期就么有太大意义了
  2. 生命周期注解的作用就在于告知编译器:某些参数它们的生命周期是一样的,你放过我吧!
  3. 对于函数/方法而言,注解标记的必要性是因为引用类型参数过多导致的,并且是在返回类型也是引用的情况下
  4. 对于其它对象(结构体、枚举等),只要有引用类型,都必须定义生命周期注解标记
  5. 也有不需要添加注解情况,通常适用于只有一个引用参数,返回也是一个引用的情况。但是情况并不是只有这种。
  6. ‘static是一个静态生命周期标记,常见的字符串字面量都是这样的。注意使用
  7. 一个方法中如果又有通用类型,又有引用,那么还是可以写出来的的,例如<'a,T>

rust为了维护程序的高效和安全,需要把大部分的问题消灭在编译阶段,所以这提高了对程序员的要求。

虽然如此,rust也提供了大概有史以来最体贴的编译器。

 

posted @ 2024-11-26 17:21  正在战斗中  阅读(21)  评论(0编辑  收藏  举报