写点什么

每日一 R「05」生命周期

作者:Samson
  • 2022 年 8 月 12 日
    上海
  • 本文字数:2600 字

    阅读完需:约 9 分钟

每日一R「05」生命周期

01-生命周期

Rust 中的值或其引用,根据其作用域的大小,可以划分为两类:


  1. 静态生命周期,即贯穿整个进程的生命周期。具有这类生命周期的值一般包括全局变量、静态变量、字符串字面量、使用 Box::leak 从堆中泄漏出去的值。静态生命周期一般通过 ‘static 或 &’static 表示,这种写法在 Rust 中称为生命周期标注。

  2. 动态生命周期,如果值或其引用是在某个作用域(例如函数作用域)中定义的,那么则称其具有动态生命周期。动态生命周期的标注一般表示为 ‘a 或 &’b,a 或 b 或者其他的什么符号都不重要。


课程中有一幅图总结了 Rust 中的生命周期与栈、堆上值的关系:



  • 分配在栈上的值的生命周期与栈帧的生命周期绑定在一起;

  • 分配在堆上的值,通过所有权、栈帧中的胖指针与栈帧的生命周期绑定在一起;

  • 堆上通过 Box::leak 显式泄漏出去的值、全局变量、静态变量、字符串字面量、代码等内容与进程生命周期一致;


引用的生命周期是 Rust 中非常重要的一块内容,编译器也正是通过引用的生命周期检查来解决悬垂指针问题的。接下来我们来看一下编译器是如何识别引用的生命周期的,以及如何避免悬垂指针的。

02-悬垂指针和生命周期标注

悬垂指针本质是引用了已经释放的值,或者换个角度讲就是试图引用比自身生命周期短的值,思考如下的示例:



变量的 r 的生命周期 ‘a 从声明开始,到 println! 结束。变量 x 的生命周期 ‘b 从变量声明开始,到第一个 } 结束。r = &x; 尝试将 r 指向 x。当 } 后,x 因为离开作用域而被释放,那 r 对 x 的引用也将不再合法。所以 r 就变成了一个悬垂指针。这在程序中是非常危险的。如果我们继续尝试读取 r,则可能会导致程序不可预测的行为。


为了保证 Rust 的所有权和借用的正确性,Rust 使用了一个借用检查器(Borrow checker),来检查我们程序的借用正确性。在编译阶段,Rust 编译器会比较变量的生命周期。如果检查不通过,则认为我们编写的程序存在风险。


大多数情况下,编译器可以通过上下文推断出变量的生命周期。但在某些情况下,编译器并不能推断出来,这时候就需要认为地对生命周期进行标注。一个常用的例子就是比较字符串切片长度的例子:


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<'a>(x: &'a str, y: &'a str) -> &'a str {fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y }}
复制代码


在编译时,编译器并无法推断出入参 x、y 和返回值之间的生命周期关系,所以会抛异常。根据我们的方法实现,返回值要么是 x,要么是 y,这取决于哪个的长度更长。这也就意味着,返回值的生命周期至少要跟 x 和 y 中较短的那个声明周期一致。我们把入参和返回值的生命周期都标注为 ‘a 就意味着,’a 是他们三个至少要满足的生命周期。


生命周期标注并不会改变任何引用的实际作用域。在通过函数签名指定生命周期参数时,我们并没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过。


再考虑如下代码:


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()); // borrowed value does not live long enough    }    println!("The longest string is {}", result); // ------ borrow later used here}
复制代码


采用之前的标注,上述代码运行会报错。我们来分析下报错原因:


  • 先来看下 longest 函数的两个入参的生命周期,假设 string1 的生命周期为 ‘a,string2 的生命周期为 ‘b,明显 ‘a 长于(overlive) ‘b。

  • 根据前面的推断,longest 函数返回值的生命周期至少跟两个入参中较短的那个一样长,所以返回值的生命周期应该至少跟 ‘b 一样长

  • result 生命周期 ‘c,从定义开始,到 println! 结束,这个显然比 ‘b 要长。

  • 当 result = longest(); 时,属于前面介绍的悬垂指针情况,将较长生命周期的引用指向较短生命周期的值,所以报错。


本节课程中有一个课后习题,我觉得应该好好分析下其过程:


// 这种标注有问题吗?pub fn strtok<'a>(s: &'a mut &str, delimiter: char) -> &'a str {    if let Some(i) = s.find(delimiter) {        let prefix = &s[..i];        let suffix = &s[(i + delimiter.len_utf8())..];        *s = suffix;        prefix    } else {        let prefix = *s;        *s = "";        prefix    }}
fn main() { let s = "hello world".to_owned(); // 1 let mut s1 = s.as_str(); // 2 let hello = strtok(&mut s1, ' '); // 3 println!("hello is: {}, s1: {}, s: {}", hello, s1, s); // 4 // cannot borrow `s1` as immutable because it is also borrowed as mutable}
复制代码


strtok 函数中有两个引用参数和一个引用返回值,按照上述的标注方式,返回值应该至少跟可变引用的生命周期一样长。


  • s 的生命周期 ‘a,从 1→4,s1 的生命周期 ‘b 从 2→4,hello 的生命周期 ‘c 从 3→4,即 ‘a > ‘b > ‘c。

  • s1 可变借用 &mut 的生命周期这里暂时记为 ‘d。

  • 根据函数中的标注,’d 至少要跟返回值 hello 的生命周期 ‘c 一样长,即’d 从 3→4。

  • 而第 4 行,println! 中又出现了 s1 的只读借用,可变借用与只读借用不可共存,所以报错。

  • 解决办法也很简单,把 s1 与 hello 分开打印。


    println!("hello is: {}, s: {}", hello, s); // 4    println!("s1: {}", s1); // 5
复制代码


分开打印为什么能行呢?s1 可变借用的生命周期到4就结束了,5行开始就没有活跃的可变借用了,所以可行了。
复制代码


上面这个过程比较难以理解,我也是看了好多遍才逐渐明朗。


Rust 会尝试对函数中引用类型的参数和返回值进行生命周期推断,推断的规则如下:


  • 所有引用类型的参数都有独立的生命周期 'a 、'b 等。

  • 如果只有一个引用型输入,它的生命周期会赋给所有输出。

  • 如果有多个引用类型的参数,其中一个是 self,那么它的生命周期会赋给所有输出。


本节课程链接:《10|生命周期:你创建的值究竟能活多久?


历史文章推荐

每日一 R「04」常用的智能指针

每日一 R「03」Borrow 语义与引用

每日一 R「02」所有权与 Move 语义

每日一 R「01」跟着大佬学 Rust

发布于: 刚刚阅读数: 2
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

还未添加个人简介

评论

发布
暂无评论
每日一R「05」生命周期_学习笔记_Samson_InfoQ写作社区