Rust 入门 生命周期

生命周期

  • Rust 中的每一个引用都有其 生命周期(lifetime)
  • 生命周期就是引用保持有效的作用域
  • 大多数情况下: 声明周期是隐式的, 可推断的
  • 当引用的声明周期可能以不同的方式互相关联时, 需要我们手动标注生命周期

声明周期避免了悬垂引用

  • 生命周期的主要作用就是为了避免悬垂引用

  • 示例: 尝试使用离开作用域的值的引用

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}
// 外部作用域声明了一个没有初化的变量 r
// 而内部作用域声明了一个初值为 5 的变量 x
// 在内部作用域中,我们尝试将 r 的值设置为一个 x 的引用
// 接着在内部作用域结束后,尝试打印出 r 的值。
// 这段代码不能编译因为 r 引用的值在尝试使用之前就离开了作用域

// 如下是错误信息:
// error[E0597]: `x` does not live long enough
//   --> src/main.rs:7:5
//    |
// 6  |         r = &x;
//    |              - borrow occurs here
// 7  |     }
//    |     ^ `x` dropped here while still borrowed
// ...
// 10 | }
//    | - borrowed value needs to live until here

// 变量 x 没有 “存在的足够久”
// 其原因是 x 在到达第 7 行内部作用域结束时就离开了作用域
// 然而 r 在外部作用域仍是有效的
// 作用域越大我们就说它 “存在的越久”
// 如果 Rust 允许这段代码工作
// r 将会引用在 x 离开作用域时 被释放的内存
// 这时尝试对 r 做任何操作都不能正常工作

借用检查器

  • Rust 编译器有一个 借用检查器(borrow checker)
  • 它比较作用域来确保所有的借用都是有效的
  • 示例: 尝试使用离开作用域的值的引用 (生命周期标注)
fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

// 将 r 的生命周期标记为 'a
// 将 x 的生命周期标记为 'b
// 可以看到 'b 块比 'a 小
// 在编译时, 借用检查器会比较这两个生命周期的大小
// 发现 r 拥有生命周期 'a 但它引用了一个拥有生命周期 'b 的对象
// 此时程序被拒绝编译, 因为生命周期 'b 比生命周期 'a 要小
// 也就是被引用的对象比它的引用者存在的时间更短。
  • 示例: 修改上述代码使其编译通过
fn main() {

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

// 将 r 的生命周期标记为 'a
// 将 x 的生命周期标记为 'b
// 此时 'b 比 'a 大
// 也就说明了在引用被使用前是有效的

函数中的泛型生命周期

  • 示例: 比较字符串长度
/// 比较字符串的长度, 返回较长的字符串引用
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

// 如下是错误信息:
// error[E0106]: missing lifetime specifier
//  --> src/main.rs:1:33
//   |
// 1 | fn longest(x: &str, y: &str) -> &str {
//   |                                 ^ expected 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`

// 编译器此时提示我们返回值需要泛型声明周期参数
// 因为 rust 不知道返回值指向的是参数x还是参数y
  • 示例: 修改上述代码使其编译通过
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
// 我们新增了一个泛型参数 'a, 表示这个函数存在一个叫做 'a 的生命周期
// 在引用符号(&)后加上生命周期名称, 就代表生命周期标注
// 这段代码表示的意思是: [x, y, 返回值]都包含相同的声明周期, 但是它们三个的生命周期并不相同

生命周期标注

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

生命周期参数名称

  • '开头 (撇号 也叫单引号)
  • 通常使用全小写的命名
  • 非常短, 通常使用一个字母表示

生命周期标注的位置

  • 在引用符号(&)之后
  • 使用空格将标注和类型分开

示例

  • &i32 一个引用
  • &'a i32 带有显示标注生命周期的引用
  • &'a mut i32 带有显示标注生命周期的可变引用

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

  • 在函数签名中必须先定义泛型生命周期参数
  • 示例:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    
}

// 首先在 <> 中声明了一个泛型生命周期参数
// x 和 y 的数据类型为 &'a str
// 返回值的类型为 &'a str

// 'a 描述了:  x 和 y 的的声明周期不能比 'a 短

深入理解生命周期

  • 指定生命周期参数的方式依赖于函数所做的事情
  • 示例:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
// 此函数中的y与返回值无关
  • 从函数中返回引用时, 返回类型的生命周期参数必须与参数中的生命周期相匹配

结构体中的生命周期标注

  • 结构体中的字段可以是自持有的类型
  • 结构体中的字段也可以是引用类型
    • 若结构体中的字段是引用类型
    • 需要手动添加声明周期标注
  • 例子:
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 };
}

生命周期的省略

  • 在 Rust 语言的早期版本中, 每个函数都必须显示的标注生命周期
  • 后来 Rust 的开发团队发现有些情况下生命周期是可以被推断的
    于是 Rust 开发团队将一些可以推断声明周期的情况写入借用检查器中
  • 例子: 早期 Rust 的函数生命周期标注(pre-1.0)
fn first_word<'a>(s: &'a str) -> &'a str {
    
}
  • 例子: 现在 Rust 的函数声明周期标注中的省略
fn first_word(s: &str) -> &str {
    
}
  • 需要注意的是: 不是所有的生命周期标注都可以省略

声明周期的省略规则

  • 被编码进 Rust 引用分析的模式被称为 生命周期省略规则(lifetime elision rules)
    • 这些规则不需要开发者来遵守
    • 这是一些特殊情况, 需要由编译器来考虑
    • 如果你的代码符合这些情况, 就不需要显示的标注生命周期
  • 声明周期省略规则不会提供完整的推断
    • 如果应用规则后, 引用的声明周期仍然模糊不清, 就会导致编译错误
    • 解决办法就是手动添加声明周期标注
  • 输入/输出声明周期:
    • 函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes)
    • 返回值的生命周期被称为 输出生命周期(output lifetimes)
  • 声明周期省略规则:
    • 每个引用类型的参数都有自己的声明周期
    • 如果只有一个输入声明周期参数, 那么该生命周期会被赋予所有的输出声明周期参数
    • 如果输入生命周期参数中包含&self或者&mut self, 那么self的声明周期会被赋给所有的输出声明周期参数
  • 编译器使用上述三条规则在没有显式标注声明周期的情况下,来确定引动的声明周期
    • 第一条规则用于输入生命周期
    • 后两条规则用于输出声明周期
    • 如果编译器使用了这三个规则之后仍然无法确定声明周期的引用, 就会编译失败
    • 这三条适用于 fn定义和impl

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

  • 在结构体上使用生命周期实现方法, 语法和泛型相同

  • 在哪声明和使用生命周期参数, 依赖于:

    • 生命周期参数是否和字段, 方法的参数或返回值有关
  • 结构体字段的生命周期名:

    • impl后声明
    • 在结构体名称后使用
    • 这些生命周期是结构体类型的一部分
  • 实现块内的方法签名中:

    • 引用必须绑定结构体字段的声明周期, 或者引用独立
    • 生命周期省略规则经常使得方法中的生命周期标注不是必须的
  • 例子:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

静态声明周期

  • 'static是一个特殊的声明周期
    • 它表示整个程序的运行时间
  • 例如:
    • 所有的字符串字面值就是'static
  • 在为引用指定'static之前一定要三思

例子:

  • 在这个例子中使用了:
    • 泛型参数类型
    • Trait Bound
    • 声明周期
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
    }
}

fn main() {
    
}
如果对我分享的内容感兴趣的话  记得关注我得动态
求推荐  求收藏  求转发  求关注
posted @ 2021-07-08 14:59  Delayer  阅读(529)  评论(0编辑  收藏  举报