六、生命周期

56 阅读5分钟

前言

阅读耗时2分钟

目录

六、生命周期.png

一、生命周期

Rust的每个引用都有自己的生命周期,所谓生命周期,就是指引用保持有效的作用域。
当引用的生命周期可能以不同的方式相互关联时,可以手动标注生命周期。

目标

生命周期的主要目标就是避免悬垂引用(dangling reference)

悬垂引用

fn main() {
    let r;
    {
        let x = 5;
        r = &x; //这里会报错
    }//x作用域结束
    println!("r: {}", r);
}

报错

error[E0597]: `x` does not live long enough
  --> src\main.rs:17:13
   |
17 |         r = &x;
   |             ^^ borrowed value does not live long enough
18 |     }
   |     - `x` dropped here while still borrowed
19 |     println!("r: {}", r);
   |                       - borrow later used here

x 的生命周期在 } 执行完后就结束了。而后面我们竟然还用了。这就是悬挂引用
为什么在编写代码时就会价差出来这个问题,会报错呢?或者说为什么编译就会出错呢?
这完全依赖于Rust的借用检查器,通过比较作用域来判断所有的借用是否合法。
如何修改呢?

let x = 5;
let r = &x;//这样就可以了
println!("r : {}", r};

被用于函数参数的生命周期

fn main() {
    let string1 = String::from("abcdef");
    let string2 = "efgi";
    let result = longest(string1.as_str(), string2);
    println!("longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str{
    if(x.len() > y.len()) { 
        x //报错
    }else{
        y // 报错
    }
}

上述报错,是因为返回值的生命周期,不知道是x的还是y的,可以手动尝试run一下,编译器会提示怎么修改
下面按编译器提示修改

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

加了一个生命周期的标注 'a 代表 xy 和返回值是一样的生命周期

二、生命周期标注

  • 生命周期的标注不会改变引用的生命周期长度
  • 描述多个引用的生命周期的关系,但不影响生命周期

标注定义

&'a test// 引用&test 到标注只要加('a空格) a换成其它小写字母都可以

函数签名生命周期标注

//方法后面要加声明
//x和y和的生命周期必须不能短于'a, 函数返回值也不能短于 'a
//'a 的生命周期其实结果是 x 和 y中最短的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str{
}

比如换一种写法

fn main() {
    let string1 = String::from("abcdef");
    let result;
    {
        let string2 = String::from("efg");
        result = longest(string1.as_str(), string2.as_str());、//这块会报错
    }
    println!("longest string is {}", result);
}

报错原因就是因为result 取的是string1string2 中最短的生命周期,result持有的借用 出了 } 就失效了。
再换一种写法看看

fn longest<'a>(x: &'a str, y: &'a str) -> &str{
    let result = String::from("abc");
    result.as_str() //这里报错
}

为什么报错?因为result.as_str()在函数方法执行完,生命周期就结束了,这块引用就被清理掉了,然后又作为引用借用返回给外部调用方。 可以通过移交所有权来修改

fn longest<'a>(x: &'a str, y: &'a str) -> String{
    let result = String::from("abc");
    result
}

从上面可以看出生命周期的定义类似泛型,就是泛型的一种方式,所以可以和泛型一起用

fn longest_t<'a, T>(x: &'a str, y: &'a str, ann:T) -> &'a str
where T:Display
{
    println!("Ann is {}", ann);
    if x.len() > y.len(){
        x
    }else{
        y
    }
}

struct定义中的生命周期标注

struct GoToWork<'a>{
    part: &'a str, //part的生命周期必须要比GoToWork实例生命周期长
}
fn main(){
    let string = String::from("testtest");
    let i = GoToWork{
        part:string.as_str()
    };
}

平时正常写的时候不会提示生命周期问题,原因就是我们用的时候,例如part生命周期比实例还长。

方法定义中的生命周期标注

struct GoToWork<'a>{
    part: &'a str, //part的生命周期必须要比GoToWork实例生命周期长
}
impl<'a> GoToWork<'a>{
    fn level(&self) -> i32{
        3
    }
    fn return_test(&self, item: &str) -> &str{
        self.part
    }
}

impl 块内的方法签名中

  • 引用必须绑定于struct字段引用的生命周期,或者引用是独立的
  • 生命周期省略规则使得方法中的生命周期标注不是必须的

三、生命周期省略

不声明生命周期也OK

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

等价于

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

其原因是Rust团队觉得每个方法或者stuct都写生命周期很繁琐,就做了一套统一的处理,直接写入编译器中。

四、生命周期规则

  • 输入生命周期
    • 生命周期在函数/方法的参数上
  • 输出生命周期
    • 生命周期在函数的返回值上

生命周期规则
规则1应用于输入生命周期,规则2、3应用于输出生命周期,如果编译器应用完3个规则后,仍然无法确定生命周期的引用,就会报错。这些规则适用于 fn 定义和 impl 块。

  • 规则1
    • 每个引用类型的参数都有自己的生命周期
  • 规则2
    • 如果只有1个输入生命周期参数,那么该生命周期被赋给所有输出生命周期
  • 规则3
    • 如果有多个输入生命周期参数,但其中一个是 &self 或者 &mut self(是方法),那么self的生命周期会被赋给所有输出生命周期

1. 一个入参的生命周期标注过程
我们看之前的first_word 方法

fn first_word(s:&str) -> &str

应用完规则1后变成

fn first_word<'a>(s:&'a str) -> &str

再应用规则2

fn first_word<'a>(s:&'a str) -> &'a str

周期应用完毕,编译器正常运行
2. 多个入参的生命周期标注过程

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

应用规则1

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

然后不符合规则2,因为是多个参数
再看规则3,不是方法,没有self参数,不符合规则,不能确定返回值的生命周期。编译器报错,需要手动标注生命周期。
如果我们再看之前 impl块的 return_test 方法,它就符合规则1 和 规则 3,所以生命周期标注省略。

五、静态生命周期

通过关键字 'static 声明,代表整个程序持续的时间。所有的字符串字面值都有 'static 生命周期。
注意,使用了这个关键字,代表的是这个引用在程序整个生命周期内都存活。

let s:&'static str = "I want to go to work!";