Rust——生命周期

简而言之,即引用的有效作用域;一般情况下编译器会自动检查推导,但是当多个声明周期存在时,编译器无法推导出某个引用的生命周期,需要手动标明生命周期。

悬垂指针

悬垂指针是指一个指针指向了被释放的内存或者没有被初始化的内存。当尝试使用一个悬垂指针时,无法保证内存中的数据是否有效,从而导致程序的不确定行为。

    {
        let r;
        {
            let x = 5;
            r = &x;
        }
        println!("r: {}", r);
    }

问题:r 引用了内部花括号中的 x 变量,但是 x 会在内部花括号 } 处被释放,因此回到外部花括号后,r 会引用一个无效的 x。此时 r 就是一个悬垂指针,它引用了提前被释放的变量 x。生命周期就是为了解决这种问题的出现。

借用检查

Rust 使用借用检查器检查程序的借用正确性。

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

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

如果想编译通过,需要使 x 变了活得比 r 久

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

生命周期

定义及语法

Rust 生命周期通过引用和所有权机制来管理内存的使用。每个变量都有其所有权,只有所有者可以释放其对应的内存。当变量被移动或引用时,其所有权也会随之转移。

需要注意的是,生命周期标注不会改变引用的实际作用域。编译器无法确定生命周期时,需要人工明确指定变量和引用的生命周期,以确保它们的使用是安全和有效的。

生命周期的语法:以 ' 开头,名称常用一个单独的小写字母如 'a 。

fn useless<'a>(first: &'a i32, second: &'a i32) {}

如果是引用类型的参数,那么生命周期会位于引用符号 & 之后,并用一个空格来将生命周期和引用参数分隔开:

&i32        // 一个引用
&'a i32     // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

函数中的生命周期

原始函数如下

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

运行时会抛出异常

error[E0106]: missing lifetime specifier
 --> main.rs:8:33
  |
8 | fn longest(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
  |
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

error: aborting due to previous error

由异常可以看到,编译器无法确定 longest 函数的返回值是引用 x 还是引用 y 来进行后续的引用生命周期分析。即在存在多个引用时,编译器有时会无法自动推导生命周期

对 longest 函数添加生命周期标注

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

  • 和泛型一样,使用生命周期参数,需要先声明 <'a>
  • x、y 和返回值至少活得和 'a 一样久

需要强调的是,在通过函数签名指定生命周期参数时没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过

当把具体的引用传给 longest 时,那生命周期 'a 的大小就是 x 和 y 的作用域的重合部分,即'a 大小为 x 和 y 中较小的那个。

如下所示

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

此时,string2 的生命周期比 string1 小,因此 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);
}

此时由于生命周期 result = string1 > string2,因此会抛出异常

 --> main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());   
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

error: aborting due to previous error

特殊场景

只返回第一个参数

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

y 完全没有被使用,即不需要为 y 标注生命周期,只需要标注 x 参数和返回值即可。

返回值为引用

若返回值为引用类型,则生命周期为

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

若为后者有可能出现悬垂指针:

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

此时抛出异常

error[E0515]: cannot return reference to local variable `result`
  --> main.rs:15:5
   |
15 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

result 在函数结束后就被释放,但是仍然存在对 result 的引用,无法指定合适的生命周期(好处是避免了悬垂引用),为了解决这种异常,可以返回内部字符串的所有权,将字符串的所有权转移给调用者。

fn longest<'a>(_x: &str, _y: &str) -> String { //此处直接返回的是String,而不是引用类型
    String::from("really long string")
}
fn main() {
   let s = longest("not", "important");
   println!("{}",s)
}

结构体中的生命周期

在结构体的字段中存在引用时,需要为结构体的每个引用标注生命周期。

#[allow(dead_code)]
#[derive(Debug)]
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,
    };
    println!("{:?}",i);
}

结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>。该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。

#[allow(dead_code)]
#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}
fn main() {
    let i; 
    {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let first_sentence = novel.split('.').next().expect("Could not find a '.'");
        i = ImportantExcerpt {
            part: first_sentence,
        };
    }
    println!("{:?}",i); // 此处使用了结构体,而里面引用的字符串
}

引用字符串在内部语句块末尾 } 被释放后,println! 依然在外面使用了该结构体,因此会导致无效的引用

error[E0597]: `novel` does not live long enough
  --> main.rs:10:30
   |
9  |         let novel = String::from("Call me Ishmael. Some years ago...");
   |             ----- binding `novel` declared here
10 |         let first_sentence = novel.split('.').next().expect("Could not find a '.'");
   |                              ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
14 |     }
   |     - `novel` dropped here while still borrowed
15 |     println!("{:?}",i);
   |                     - borrow later used here

生命周期的消除规则

对于编译器来说,每一个引用类型都有一个生命周期,但是在编写程序时无需标注

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 函数,它的返回值是一个引用类型,那么该引用只有两种情况:

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

当返回值的引用是来自参数时,说明参数和返回值的生命周期是一样的。因此不标注也不会产生歧义。

消除规则

注意点:

  • 若编译器不能确定某件事是正确时,会直接判为不正确,此时需要手动标注生命周期
  • 函数或者方法中,参数的生命周期被称为输入生命周期,返回值的生命周期被称为输出生命周期

三条消除规则

  • 每一个引用参数都会获得独自的生命周期(输入)
  • 若只有一个输入生命周期(函数参数中只有一个引用类型),该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期(输出)
  • 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期(输出)
    注:存在&self 说明该函数是一个方法。如果返回值生命周期和 &self 不一样需要手动标注。

方法中的生命周期

为具有生命周期的结构体实现方法时,使用的语法跟泛型参数语法很相似

struct ImportantExcerpt<'a> {
    part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

注意点:

  • impl 中必须使用结构体的完整名称,包括 <'a>,因为生命周期标注也是结构体类型的一部分
  • 方法签名中,由于生命周期消除的第一和第三规则一般不需要标注生命周期

编译器应用规则步骤:
原始代码

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 { //不知道announcement生命周期,因此重新赋予生命周期b
        println!("Attention please: {}", announcement);
        self.part
    }
}

编译器应用第三规则,将 &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
    }
}

如果手动标注返回值的生命周期为 'b,此时会报错,这是由于编译器无法知道 'a 和 'b 的关系。由于 &'a self 是被引用的一方,因此引用它的 &'b str 必须要活得比它短,否则会出现悬垂引用,因此需要使生命周期 'b 必须要比 'a 小。

方法 1:生命周期约束语法'a: 'b,说明'a 必须比 'b 活得久;把 'a 和 'b 都在同一个地方声明

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

方法 2:分开声明但通过 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
    }
}

静态生命周期

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

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

可以帮助解决非常复杂的生命周期问题甚至是无法被手动解决的生命周期问题

'static,有两种用法: &'static 和 T: 'static

&'static

一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static。主要注意的是, &'static 生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则:

use std::{slice::from_raw_parts, str::from_utf8_unchecked};

fn get_memory_location() -> (usize, usize) {
  // “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
  // 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
  let string = "Hello World!";
  let pointer = string.as_ptr() as usize;
  let length = string.len();
  (pointer, length)
  // `string` 在这里被 drop 释放
  // 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
}

fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
  // 使用裸指针需要 `unsafe{}` 语句块
  unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}

fn main() {
  let (pointer, length) = get_memory_location();
  let message = get_str_at_location(pointer, length);
  println!(
    "The {} bytes at 0x{:X} stored: {}",
    length, pointer, message //The 12 bytes at 0x563D1B68305B stored: Hello World!
  );
}

可以看到持有 &'static 引用的变量的生命周期受到作用域的限制

T: 'static

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: T) {
    println!( "'static value passed in is: {:?}", input );
}

fn print_it1( input: impl Debug + 'static ) {
    println!( "'static value passed in is: {:?}", input );
}

fn main() {
    let i = 5;
    print_it(&i);
    print_it1(&i);
}

上述两种形式的代码中,T: 'static 与 &'static 有相同的约束:T 必须活得和程序一样久;该代码会报错,&i 的生命周期无法满足 'static 的约束

修改方法一:将 i 修改为常量

fn main() {
    const I: i32 = 5;
    print_it(&I);
}

修改方法二: 将 T 修改为 &T

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: &T) {
    println!( "'static value passed in is: {:?}", input );
}

fn main() {
    let i = 5;
    print_it(&i);
}

原因在于约束的是 T,但是使用是它的引用 &T,即没有直接使用 T,因此不会检查 T 的生命周期约束。

总而言之,&'static 引用指向的数据活得跟程序一样久,引用本身是要遵循其作用域范围的。

posted @ 2023-07-31 13:29  JICEY  阅读(184)  评论(0编辑  收藏  举报