Rust-生命周期与引用有效性

  Rust中的每个引用都是有其 生命周期 (lifetimes),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于当因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期以一些不同方式相关联的情况,所以Rust需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

  据了解,在 Rust 官方的一次调查中,当受访者被问及对于提升 Rust 的采用率有何建议时,许多人提到的一个方案是降低 Rust 的学习难度。再具体到特定主题的难度时,许多人认为 Rust 的“生命周期(Lifetimes)”难度最高,其次是 Ownership,61.4% 的受访者表示,生命周期的使用既棘手又非常困难。而这两个功能又恰恰是 Rust 内存安全特性的核心。

  生命周期的概念从某种程度上说不同于其它语言中类型的工具,毫无疑问这是Rust最与众不同的功能。

生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。如下示例,它有一个外部作用域和一个内部作用域:

{
    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:245:13
    |
245 |         r = &x;
    |             ^^ borrowed value does not live long enough
246 |     }
    |     - `x` dropped here while still borrowed
247 |     println!("r:{}", r);
    |                      - borrow later used here

 变量x并没有"存在的足够久"。其原因是x在到达第246行内部作用域结束时就离开了作用域。不过r在外部作用域仍是有效的;作用域越大我们就说它“存在的越久”。如果Rust允许这段代码工作,r将会引用在x离开作用域时被释放的内存,这时尝试对r做任何操作都不能正常工作。那么Rust是如何决定这段代码是不被允许的呢?这得益于借用检查器。

借用检查器

Rust编译器有一个借用检查器(borrow checker),它比较作用域用确保所有的借用都是用效的。

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

r和x的生命周期注解,分别叫做 'a 和 'b

这里将r的生命周期标记为'a并将x的生命周期标记为'b。如上面的展示,内部的'b块要比外部的生命周期'a小得多。在编译时,Rust比较这两个生命周期的大小,并发现r拥有生命周期'a,不过它引用了一个拥有生命周期'b的对象。程序被拒绝编译,因为生命周期'b比生命周期'a要小:被引用的对象比它的引用者存在的时间更短。

让我们看看以下并没有产生悬垂引用且可以正确编译的例子:

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

这里x拥有生命周期'b,比'a要大。这就意味着r可以引用x:

Rust知道r中的引用在x有效的时候也总是有效的。

Lifetime Bound

就像其它泛化类型参数类型,可以使用lifetime bound来约束一个类型T或另一个lifetime 'b,以要求类型T或'b满足一定的条件;

使用 : 来表示bound约束,类似与类型参数/Trait的bound约束,lifetime bound有如下形式及语义:

'a: 'b 表示lifetime 'a必须outlive lifetime 'b即'a的范围至少与'b的范围一样大;

T: 'a 表示类型T中包含的所有引用对应的lifetime必须outlive lifetime 'a即包含的引用在lifetime 'a内可被有效访问;

T: Trait + 'a 表示类型T必须实现trait Trait同时T包含的所有引用在lifetime 'a 内可被有效访问;

struct Ref<'a, T: 'a>(&'a T) ;

上面Ref定义表示:结构体Ref包含一个指向泛化类型T对象实例的引用,该引用具有lifetime 'a,对应的T对象实例具有lifetime 'a,类型T内所有的引用必须outlive lifetime 'a;

另外Ref结构体的对象实例本身不能outlive lifetime 'a;

另外出现'a: 'a从语法上是合理的,表示lifetime 'a的范围至少与自身的范围一样大,但'a上bound后,则它变成early bound;

lifetime Elision

虽然每一个函数中涉及到的引用参数都可以显式的使用lifetime标记anotations,但在有些场景下可以省略annotations,由编译器自动推导生成一个annotation,以便减少代码的编写和减轻开发者的负担;

触发lifetime Elision的规则如下:

  • 函数的每一个引用参数都要有自身对应的lifetime;
  • 如果函数正好只有一个引用输入参数,则所有引用输出参数都使用输入参数lifetime;
  • 如果函数有多个输入参数,其中有一个为&self或&mut self,则所有引用输出参数都使用&self或&mut self的lifetime;
fn first_word(s: &str) -> &str {}
//等价
fn first_word<'a>(s: &'a str) -> &'a str {}

由于没法满足B,C规则,上面提到的longest函数中必须显示写出lifetime标记,否则会编译出错,

因为不显示标记,则无法确定两个输入参数和一个输出参数之间的lifetime约束;

如下示例不显示标记,不能通过编译:

fn longest(x: &str, y: & str) -> & str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
error[E0106]: missing lifetime specifier
   --> src/main.rs:160:34
    |
160 | 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
    |
160 | fn longest<'a>(x: &'a str, y: &'a  str) -> &'a  str {
    |           ^^^^    ^^^^^^^     ^^^^^^^^     ^^^

加上lifetime标记后通过编译:

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

 特殊标记'static

静态生命周期'static:是Rust内置的一种特殊的生命周期。'static生命周期存活于整个程序运行期间。所有的字符串字面量都有生命周期,类型为 & 'static str。

标记 'static 作为Rust语言保留的lifetime标记标识,可用来表示引用的lifetime,可以用作lifetime bound来约束其它类型;

/*
    A reference with 'static lifetime:
    表示被引用的对象在程序整个生命周期都有效;
    引用本身可在程序退出前的整个生命周期都可安全有效使用;
    具有'static的引用可以转换成更小的子范围/lifetime 'b引用并被使用;
    */
    let s: &'static str = "hello world";
/*
'static as part of a trait bound:
'static用作bound,表示类型T没有包含非'static引用,
作为类型T的对象实例接受都可在不同上下文中使用该实例,直到析构该实例;
而不是表示类型T的对象实例的生命周期是'static;
*/
fn generic<T>() where T: 'static {}

lifetime标记用作结构体泛化参数 

泛化结构体可使用lifetime标记annotations比如'a、'b作为泛化参数,它作为一个可后续具体化的lifetime参数,其字段可以使用它作为引用类型变量定义的一部分;

struct ImportantExcerpt<'a>{
    part: &'a str
}
    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,
    };

上面示例代码表达的语义是:

结构体ImportantExcerpt对象实例不能outlives它的子字段part对应的引用的lifetime 'a;

子字段part在lifetime 'a内有效,可接受来自于lifetime 'a的str对象的引用赋值,或者在lifetime 'a范围内安全读取或为lifetime 'a范围内的引用变量赋值;

这个lifetime 'a同时对结构体ImportantExcerpt类型的对象实例和其它字段part的lifetime进行lifetime 'a约束;

为什么这个'a需要对结构体ImportantExcerpt类型的对象实例和字段part都需要产生约束呢?

其实从语义上理解非常的直接,因为如果结构体的对象实现容许在lifetime 'a之外使用,那么通过这个对象访问其子字段理应同样被容许,但从定义上要求子字段的有效lifetime为'a,现在超出了'a代表的范围访问,显然会导致悬空引用,所以逻辑上不容许,于是对结构体ImportantExcerpt的对象实例也要产生约束;

在实例ImportantExcerpt对象前需要early bound提前确定lifetime 'a的值,其具体值由创建时传递的参数first_sentence来决定;

 trait及impl中使用lifetime标记

由于lifetime标记'a是以泛化参数的形式出现在结构体ImportantExcept定义中,是结构体泛化参数的一部分;

类似与泛化中的类型参数,在impl method或trait时,同样需要以泛化参数的形式比如impl<'a>满足语法上的要求;

为结构体提供level方法实现的示例代码如下:

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

深入学习

原文地址:https://www.cnblogs.com/johnnyzhao/p/15329975.html