21_rust_生命周期

生命周期

生命周期应是rust语言最与众不同的概念。

  • rust的每个引用都有自己的生命周期。
  • 生命周期:引用保持有效的作用域。
  • 大多数情况:生命周期是隐式的、可被推断的。
  • 当引用的生命周期可能以不同的方式相互关联时,需要手动标注生命周期。

生命周期存在的目的:避免悬垂引用(dangling reference)
如:

fn main() {
  let r;
  {
    let x = 5;
    r = &x; // 编译报错
  }
  println!("{}", r);
}

这里编译报错,因为当最后一行使用r的时候的,r所指向的x已经释放了,生命周期已结束,再继续使用r将可能访问到已释放的内存的错误。rust采用了借用检查器来检查内存问题。

借用检查器

rust编译器的借用检查器:比较作用域来判断所有的借用是否合法。
如前面例子,编译器会检查r和x的生命周期,r是x的引用,但却比x存活时间更长,编译就无法通过了。

函数中的泛型生命周期

继续从一个例子开始,利用函数longer_str返回一个更长的字符串,参数是两个字符串切片。

fn main() {
    let s1 = String::from("abc");
    let s2 = "bcd";
    let ret = longer_str(s1.as_str(), s2);
    println!("{}", ret);
}
fn longer_str(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
/*编译报错:
error[E0106]: missing lifetime specifier
 --> src/main.rs:7:36
  |
7 | fn longer_str(x: &str, y: &str) -> &str {
  |                  ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
7 | fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
*/

编译报错:missing lifetime specifier,意思是缺少生命周期的标注,帮助信息可看出说函数的返回值包含一个借用值,但函数的签名却没有说明是借用x还是y,考虑引入一个命名的生命周期参数'a
看longer_str的返回值可能是x也可能是y,而x和y的生命周期在函数作用域内也无法判断,借用检查器也无法判断出返回值是的生命周期是x还是y。其实如果让函数只返回x:

fn longer_str(x: &str, y: &str) -> &str {
    x
}

报错也是一样的,因为函数签名依然无法判断出x的生命周期,需要通过泛型生命周期参数来标注:

fn main() {
    let s1 = String::from("abc");
    let s2 = "bcd";
    let ret = longer_str(s1.as_str(), s2);
    println!("{}", ret);
}
// 表示x和y的生命周期与泛型标注'a的生命周期一致
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

生命周期的标注语法

  • 生命周期的标注不会改变引用的生命周期长度
  • 当指定了泛型生命周期参数,函数可接收带有任何生命周期的引用
  • 生命周期的标注,描述了多个引用的生命周期间的关系,但不影响生命周期

生命周期标注——语法

  • 生命周期参数名:
    • '号开头
    • 通常全小写且非常短
    • 常用'a标注
  • 生命周期标注的位置:
    • 在引用的&符号后
    • 使用空格将标注和引用类型分开
&i32 //一个引用
&'a i32 //带有显式生命周期的引用
&'a mut i32 //带有显式生命周期的可变引用

单个生命周期标注本身没有意义,因为标注的目的是提供参数变量之间的生命周期关系。比如前面的例子,x和y的生命周期必须与泛型标注'a的生命周期一致才行。

函数签名中的生命周期标注

泛型生命周期参数声明在:函数名和参数列表之间的<>里。

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

例子中所表达的意思是,函数参数x、y和返回值的生命周期都不能短于'a。不过手动标注声明周期'a并未改变返回值、x和y的生命周期,只是向借用检查器指出了一些可用于借用检查的非法约束,在longer_str函数里,并不需要知道x、y的存活时长,而只需知道某个作用域能够代替'a,同时满足函数的签名约束。例子中生命周期'a的实际生命周期是x和y两个生命周期中比较小的那个:

fn main() {
    let s1 = String::from("abcd");
    let ret;
    {
        let s2 = String::from("bcd");
        ret = longer_str(s1.as_str(), s2.as_str());
    }
    println!("{}", ret);
}
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
/*编译报错:
error[E0597]: `s2` does not live long enough
 --> src\main.rs:6:39
  |
5 |         let s2 = String::from("bcd");
  |             -- binding `s2` declared here
6 |         ret = longer_str(s1.as_str(), s2.as_str());
  |                                       ^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `s2` dropped here while still borrowed
8 |     println!("{}", ret);
  |                    --- borrow later used here
*/

上面的例子修改s2不再是字面常量,而是也是个String类型,这样使得s2的作用域仅在里边大括号内,最后编译报错如附文注释所示,意思就是报调用点s2的生命周期不够,它被调用函数内借用,但在打印点未存活,虽然看代码可看出会返回s1的借用,但编译阶段并不识别这种情况,可见的确取了更短的那个生命周期作为函数longer_str内的作用域。

深入理解生命周期

1)指定生命周期参数的方式依赖于函数所做的事情。
2)函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配。
3)如果返回的引用没有指向任何参数,那它只能引用函数内创建的值,则为悬垂引用,因为函数调用结束时,就离开了作用域。

fn main() {
    let s1 = String::from("abcd");
    let ret = longer_str(s1.as_str());
    println!("{}", ret);
}
fn longer_str<'a>(x: &'a str) -> &'a str {
    let ret = String::from("abc");
    ret.as_str()
}
//编译报错:
/**
error[E0515]: cannot return reference to local variable `ret`
 --> src\main.rs:8:5
  |
8 |     ret.as_str()
  |     ^^^^^^^^^^^^ returns a reference to data owned by the current function
 */

上面例子就是返回了一个内部变量的引用,并给了main函数的ret,且还在使用,但函数内部是局部变量,内存已经释放,如果想返回函数内部的值,直接返回即可,发生move所有权的操作:

fn main() {
    let s1 = String::from("abcd");
    let ret = longer_str(s1.as_str());
    println!("{}", ret);
}
fn longer_str<'a>(x: &'a str) -> String {
    let ret = String::from("abc");
    ret
}

移交所有权后,内存清理由调用方清理。
生命周期的语法就是用来关联函数参数与返回值之间生命周期的,一旦获得了某种联系,rust就获得了足够的信息,来支持保证内存安全操作,避免悬垂指针及其他违反内存安全的错误。

Struct定义中的生命周期标注

Struct里可包括:

  • 自持有的类型
  • 引用:需要在每个引用上添加生命周期标注
struct St1<'a> { // 和函数标注一样
    f1: &'a str,
}
fn main() {
    let s1 = String::from("ab.cd");
    let tmp = s1.split('.').next().expect("no '.' found");
    let st1 = St1 {
        f1: tmp, // tmp的生命周期比st1长
    };
}

生命周期的省略

  • 每个引用都有生命周期
  • 需要为使用生命周期的函数或struct指定生命周期参数
fn test_func(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

但是上面例子是可编译通过的,其实在rust的早期版本(1.0)的时候是编译不通过的,那时候要求每个引用必须有个显式的生命周期,需要程序员手动对所有引用标注生命周期:

fn test_func<'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程序员做了很多重复的生命周期标注的事情,而且这些场景是可预测的,后来rust编译团队将这些场景写入了借用检查器,使得编译器能够对类似的场景的生命周期进行自动推导,无需再标注。后期应会有更多的场景被固化,需要手动标注声明周期的地方越来越少。
在rust引用分析中所编入的模式称之为生命周期省略规则。这些规则无需开发者遵守,多为一些特殊情况,由编译器负责推断,针对符合规则的代码则无需手动标注生命周期了。不过生命周期省略规则不会提供完整的推断,若应用规则后依然无法确定,则编译报错,此时仍需手动标注。

输入、输出生命周期

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

生命周期省略的三个规则

编译器使用3个规则应用于无显示标注生命周期的情况,来确定引用的生命周期:

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

规则:

  • 规则1:每个引用类型的参数都有自己的生命周期
  • 规则2:如果只有1个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
  • 规则3:如果有多个输入生命周期参数,但其中一个是&self或&mut self(是方法),那么self的生命周期会被赋给所有的输出生命周期参数
    例子1:
fn func(s: &str) -> &str {} // 有这样一个函数
fn func<'a>(s: &'a str) -> &str {} // 首先应用规则1,给所有输入生命周期进行标注,每个参数有自己的生命周期
fn func<'a>(s: &'a str) -> &'a str {} // 因为只有1个参数,所以可应用规则2,给返回值标注生命周期
//至此所有输入输出参数都有了生命周期,无需应用第三条规则,编译器继续分析代码

例子2:

fn longer(x: &str, y: &str) -> &str {} // 一个两个参数的函数
// 首先应用第一条规则给所有输入参数标注,因为有两个参数,所以有两个生命周期
fn longer<a, b>(x: &:'a str, y: &:'b str) -> &str {}
// 由于有两个生命周期标注,第二条规则就不适用了。
// 而且又因longer是一个函数而不是方法,没有self参数,因此第三条规则也无法应用。
// 尝试三条规则后,发现仍然无法确定返回值的生命周期,所以编译报错。

总结:编译器尝试应用所有生命周期规则之后,仍然无法判断出签名的所有引用的生命周期时,则编译报错。

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

在struct上使用生命周期实现方法,语法和泛型参数的语法一样,
在哪里声明和使用生命周期参数,依赖于:生命周期参数是否和字段、方法的参数或返回值有关。
struct字段的生命周期名:

  • 在impl后声明
  • 在struct名后使用
  • 这些生命周期是struct类型的一部分

impl块内的方法签名中:

  • 引用必须绑定于struct字段引用的生命周期,或者引用是独立的
  • 生命周期省略规则经常使得方法中的生命周期标注不是必须的
struct St2<'a> {
    p: &'a str,
}
// struct字段的生命周期的名字总是应用于impl关键字后及结构体名后
impl<'a> St2<'a> {
    // 前面两个'a是不能忽略的,但根据省略规则,无需标注func1的生命周期
    fn func1(&self) -> i32 { //不会引用任何东西
        3
    }
    // 根据第一条省略规则会给self和a进行生命周期标注,根据第三条规则,返回值会被赋予self参数的生命周期
    fn func2(&self, a: &str) -> &str { // 这样所有的生命周期都计算出来了,编译通过
        println!("{}", a);
        self.p
    } 
}

静态生命周期

static是一个特殊的生命周期:为整个程序的持续时间。
比如,所有字符串字面值都拥有"'static"生命周期:

let s: &'static str = "test";

它是被存储在二进制程序中的,所以总是可用的,因此字符串的生命周期都是'static

泛型参数类型、Trait Bound、生命周期的例子

use std::fmt::Display;
fn longer<'a, T> (x: &'a str, y: &'a str, z: T) -> &'a str
where
    T: Display,
{
    println!("{}", z);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
posted @ 2023-10-27 00:37  00lab  阅读(16)  评论(0编辑  收藏  举报