zoukankan      html  css  js  c++  java
  • 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
        }
    }

    深入学习

  • 相关阅读:
    2015年蓝桥杯省赛A组c++第3题
    2015年蓝桥杯省赛A组c++第1题
    算法学竞赛常用头文件模板
    Android+Tomcat通过http获取本机服务器资源
    Ubuntu16.04LTS卸载软件的命令
    20个有趣的Linux命令
    Ubuntu16.04开机蓝屏问题解决
    Ubuntu启动时a start job is running for dev-disk-by延时解决
    Cookie与Session
    JS 闭包
  • 原文地址:https://www.cnblogs.com/johnnyzhao/p/15329975.html
Copyright © 2011-2022 走看看