Skip to content

生命周期

生命周期主要就是为了保证正确引用, 确保不会出现悬垂引用(dangling references)

rust
fn main() {
    let mut a = &10;
    {
        let b = 20;
        a = &b;
        // 编译器报错: b dose not live long enough(变量 b 存活的时间不够长)
        // a 的生命周期是整个 main 函数体, 而 b 的生命周期却只有
        // 这个大括号这么长, 当代码执行到这个大括号后面时, 里面的
        // 变量就被销毁了(内存被释放), 所以编译器就会报错
        // 编译器能够自动检测出来的, 可以比较两个变量的声明周期长度
        // 如果需要这个代码通过编译, 只需要让他们的生命周期一样长即可
    }
    println!("a = {}", a);
}
rust
fn main() {
    let mut a = &10;
    let b = 20;
    a = &b;
    // 此时他们的生命周期是一样长的, 所以不会报错
    println!("a = {}", a);
}

函数中生命周期标注

  • 类似泛型, 在函数名后的 <'x> 中标注
rust
// 此时编译器会报错: missing lifetime specifier
// 因为函数的参数作为返回值返回了(参数可以看做函数的局部变量)
// 但是并没有标注生命周期, 那么当函数执行完之后函数的所有参数
// 都应该被销毁掉, 那么就相当于返回一个被销毁的变量的引用,
// 这样就会导致悬垂引用(空指针)
fn longset(s1: &str, s2: &str) -> &str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}
rust
// 's 表示: 告诉编译器我要将参数当作返回值, 在函数执行完时,
// 不要自动销毁这个变量, 传入的引用什么时候销毁, 你就什么时候销毁
fn longset<'s>(s1: &'s str, s2: &'s str) -> &'s str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = "abc";
    let s2 = "abcd";
    println!("{}", longset(s1, s2)); // abcd
}

声明周期注解语法

  • 生命周期标注不会改变引用的生命周期长度

  • 当指定了泛型生命周期参数, 函数可以接受带有任何生命周期的引用

  • 生命周期的标注: 描述了描述了多个引用的生命周期的关系, 但并不影响生命周期

  • 生命周期参数名:

    • ' 开头
    • 全部小写且非常短
    • 常见的使用 'a, 但是可以使用别的比如: 's
  • 生命周期标注位置:

    • 在引用的 & 符号后
    • mut 关键字前
rust
&i32        // 一个引用
&'a i32     // 带有显示生命周期标注的引用
&'a mut i32 // 带有显示生命周期标注的可变引用

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

  • 's 会获取所有使用这个标注的参数(s1 s2)的生命周期
  • 如果两个变量的生命周期不一样长, 那么取生命周期较短的那个作为标准
rust
fn longset<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

// s1 和 s2 生命周期是一样的
// 那么此时的 'a 生命周期标注代表的是
// 整个 main 函数体 {} 的范围
fn main() {
    let s1 = "abc";
    let s2 = "abcd";
    println!("{}", longset(s1, s2)); // abcd
}
rust
fn longset<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

// 此时的 'a 生命周期标注代表的是, 当前的这个 {} 的范围
// 因为 s2 比 s1 生命周期要短, 如果两个变量的生命周期不一样长,
// 那么取生命周期较短的那个作为标准
fn main() {
    let s1 = String::from("hello");
    { // 生命周期标注代表的范围
        let s2 = String::from("s2-hello");
        let res = longset(&s1, &s2);
        println!("longest:{}", res);
    } // 到此处就结束
}
rust
fn longset<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

fn main() {
    let s1 = String::from("hello");
    let mut res: &str;
    {
        let s2 = String::from("s2-hello");
        res = longset(&s1, &s2);
    }

    // 此时已经超出了生命周期标注代表的作用域范围, 因为
    // s1 和 res 的生命周期都是整个main函数, 而 s2 的生命周期只有 {}
    // 因为返回值也是标注了 'a, 所以它也是: 取生命周期较短的那个作为标准
    // 所以此时的 res 生命周期已经结束了, 再去获取就会报错
    println!("longest:{}", res);
}

深入了解生命周期

  • 指定生命周期的真缺方式是由函数具体实现的功能决定的
rust
fn longset<'a>(s1: &'a str, s2: &str) -> &'a str {
    s1
}

// 虽然传入了2个参数, 但是没有用到 s2,
// 所以s2也就不需要标注生命周期了
// 那么 'a 代表的就是 s1 的生命周期
// 所以这段代码就能够通过编译
fn main() {
    let s1 = String::from("hello");
    let mut res: &str;
    {
        let s2 = String::from("s2-hello");
        res = longset(&s1, &s2);
    }
    println!("longest:{}", res);
}

结构体定义中的生命周期标注

之前我们自定义过有数据所有权类型的结构体, 其实也可以定义 包含引用 的结构体

rust
struct User {
    id: u32,
    email: String,
}

// 之前没有定义过这种
struct Student<'a> {
    id: u32,
    name: &'a str
}

fn main() {
    let u = User {
        id: 1001,
        email: String::from("example@example.com"),
    };
    println!("email: {}", u.email);

    let s = Student {
        id: 1002,
        name: "John Doe",
    };
    println!("name: {}", s.name);
}

生命周期的省略规则

  1. 每个引用都有生命周期
  2. 需要为使用生命周期的函数/struct标注生命周期参数
rust
// 按照道理来说, 这个代码必须要标注生命周期才能通过编译
// 传入的是 &str 是一个引用, 返回的 &str 也是一个引用
// 但是, 现在不用手动的标注生命周期参数, 也能编译
// --------这是因为,所有的引用都手动标注未免太繁琐
// 所以 rust 编译器,会将一些符合特定规则的生命周期参数(包括参数/返回值)
// 直接写进了编译器中,当遇到这些规则时,会自动添加标注
// 符合这些规则的,就是: 生命周期的省略规则
// 但是如果代码不符合这些规则, 那么编译器无法自动添加标注, 就会报错
#![allow(unused)]
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[..]
}

输入/输出生命周期

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

生命周期的省略规则

  1. 每个引用类型的参数都有自己的生命周期(只能用于 输入生命周期)
  2. 如果只有一个输入生命周期, 那这个唯一的生命周期赋给所有的输出生命周期(如: first_word 例子)
  3. 如果有多个输入生命周期参数, 但是其中一个是 &self 或者 &mut self, 那这个 &slef 的生命周期就会赋给所有的输出生命周期

静态生命周期

'static 是一个特殊的生命周期, 它表示整个程序的的持续时间

rust
fn main() {
  // 字符串切片就是一个静态的生命周期
  // 因为: 字符串切面是直接编译在二进制可执行程序中的
  let msg: &'static str = "hello";
}

谨慎使用静态生命周期, 因为并不是所有的引用都需要在程序运行时都存活的

Released under the MIT License.