深入了解Rust的生命周期

目录
  • 楔子
  • 生命周期标注语法
  • 结构体中的生命周期标注
  • 生命周期的省略
  • 方法中的生命周期标注
  • 同时指定生命周期和泛型

楔子

Rust 的每个引用都有自己的生命周期,生命周期指的是引用保持有效的作用域。大多数情况下,引用是隐式的、可以被推断出来的,但当引用可能以不同的方式互相关联时,则需要手动标注生命周期。

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }  // 此处 r 不再有效
    println!("{}", r);
}

执行的时候会报出如下错误:borrowed value does not live long enough,意思就是借用的值存活的时间不够长。因为把 x 的引用给 r 之后,x 就被销毁了,那么 r 就成为了一个悬空引用。

而 Rust 会通过借用检查器,来检查借用是否合法,显然上述代码在执行打印语句的时候,r 已经不合法了。

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

这段代码也是不合法的,原因就是返回值要么是 x 要么是 y,但具体是哪一个不知道,并且它们的生命周期也都不知道。所以无法通过比较作用域,来判断返回的引用是否是一致有效的,而借用检查器也是做不到的,原因就是它不知道返回值的生命周期是跟 x 有关系还是跟 y 有关系。事实上,这个跟函数体的逻辑也没有关系,函数的声明就决定了它做不到这一点。

因此我们需要引入生命周期。

生命周期标注语法

首先生命周期标注并不会改变引用的生命长度,当指定了生命周期参数,函数可以接收带有任何生命周期的引用。生命周期的标注:描述了多个引用的生命周期间的关系,但不影响生命周期本身。

现在光读起来可能有点绕,别急,一会儿会解释。

生命周期参数名以 ' 开头,并且名字非常短,通常为 a;标注位置在 & 后面,只有 & 才需要生命周期。因为你引用了一个值,那么这个值的存活时间需要知道,不然人家都被销毁了还傻傻地用。

  • &i32:一个引用;
  • &'a i32:带有显式生命周期的引用;
  • &'a mut i32:带有显式生命周期的可变引用;

其实单个生命周期标注本身没有什么意义,它是为了向 Rust 描述多个具有生命周期的参数之间的关系。并且生命周期和泛型一样,也要声明在尖括号内。

// 签名里面的生命周期必须要有
// 相当于告诉 Rust 有这么一个生命周期 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

此时代码是合法的,但是注意:我们并没有改变传入的值和返回的值的生命周期,我们只是向借用检查器指出了一些用于检查非法调用的一些约束而已,而借用检查器并不需要知道 x、y 的具体存活时长。

而事实上如果函数引用外部的变量,那么单靠 Rust 确定函数和返回值的生命周期几乎是不可能的事情。因为函数传递什么参数都是我们决定的,这样的话函数在每次调用时使用的生命周期都可能发生变化,正因如此我们才需要手动对生命周期进行标注。

// 准确来说 'a 指的就是 x 和 y 生命周期重叠的那一部分
// 而返回值的生命周期不能超重叠的部分
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let x = String::from("hello");
    {
        let y = String::from("satori");
        let result = longest(&x, &y);
        println!("result = {}", result);
        // result = satori
    }
}

目前是没有问题的,因为 x 和 y 的生命周期重叠的部分是 y,然后返回值 result 和 y 也是一样的。但如果我们把代码改一下,将 println! 语句移到花括号外面:

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

fn main() {
    let x = "hello".to_string();
    let result;
    {
        let y = "satori".to_string();
        result = longest(&x, &y);
    }
    println!("result = {}", result);  

此时就报错了:borrowed value does not live long enough。相信你已经猜到了,因为 x、y 生命周期重叠的部分是 y,返回值 result 的生命周期不能超过它。但当前明显超过了,所以报错。

所以说生命周期标注对变量没有什么影响,它只是给了借用检查器一个可以用来判断的约束罢了。

总结一下就是:生命周期用来关联函数参数和返回值之间的联系,一旦它们取得了某种联系,那么 Rust 就获得了足够多的信息来保证内存安全的操作,并且阻止那些出现悬空指针或者其它导致内存安全的行为。

到目前为止,你也许还不太了解生命周期,别着急,我们继续往下看。

结构体中的生命周期标注

struct 里面可以放任意类型,但是不能放引用,比如下面的结构体定义就是错误的。

struct Girl {
    name: &str,
    age: i32
}

结构体如果是合法的,那么它内部的所有成员值都要是合法的。但现在 name 是一个引用,所以结构体实例化的时候一定会引用某个字符串,这就使得字符串存活是结构体实例存活的前提。

但在实际编码中,这两者的存活时间没有什么关系,有可能你在使用结构体实例访问 name 成员的时候,它引用的字符串都已经被销毁了。所以 Rust 不允许我们这么做,我们之前是将 name 的类型指定为 String,也就是让结构体持有全部数据的所有权。

而如果非要将类型指定为引用的话,那么必须指定生命周期。

// 实例.name 会引用外部的一个字符串,所以要指定生命周期
// 表示字符串的存活时间一定比结构体实例要长
// 否则字符串没了,而实例还在,那么就会出现悬空引用
#[derive(Debug)]
struct Girl<'a> {
    name: &'a str,
    age: i32
}

fn main() {
    let g;
    {
        let name = String::from("古明地觉");
        g = Girl{name: &name, age: 16};
    }
    println!("{:?}", g);
}

因为指定了生命周期,在编译的时候借用检查器就可以检测出存活时间是否合法。首先 g 的存活时间是整个 main 函数,而 name 的存活时间是内部的花括号那一段作用域,比 g 的存活时间短,因此编译出错。

所以通过生命周期标注,Rust 在编译期间就能通过借用检查器检测出引用是否合法,Rust 不会将这种错误留到运行时。

生命周期的省略

当一个函数返回了一个引用时,往往需要指定生命周期,而它的目的就是为了保证返回的引用是合法的。如果不合法,在编译阶段就能找出来。

fn f(s: &str) -> &str {
    s
}

函数参数出现了引用,返回值也有引用,应该指定生命周期呀。是的,在早期版本这段代码是编译不过的,它需要你这么写:

fn f<'a>(s: &'a str) -> &'a str {
    "xxx"
}

但是久而久之,Rust 团队发现对于这种场景实在没有必要一遍又一遍的重复编写生命周期,并且这种只有一个参数完全是可以预测的,有明确的模式。于是 Rust 团队就将这些模式写入了借用检查器,可以自动进行推导,而无需显式地写上生命周期标注。

所以在 Rust 引用分析中编入的模式被称为生命周期省略规则:

  • 这些规则无需开发者来遵守;
  • 对于一些特殊情况,由编译器来考虑;
  • 如果你的代码符合这些规则,就无需显式标注生命周期;

如果生命周期在函数/方法的参数中,则被称为输入生命周期;在函数/方法的返回值中,则被称为输出生命周期。而 Rust 要能够在编译期间基于输入生命周期,来确定输出生命周期,如果能够确定,那么便是合法的。

而当我们省略生命周期时,Rust 就会基于内置的省略规则进行推断,如果推断完成后发现引用之间的关系还是模糊不清,就会出现编译错误。而解决办法就需要我们手动标注生命周期了,表明引用之间的相互关系。

那么 Rust 省略规则到底是怎样的呢?

  • 规则一:每个引用类型的参数都有独自的生命周期;
  • 规则二:如果只有一个参数具有生命周期,或者说只有一个输入生命周期,那么该生命周期会赋值给所有的输出生命周期;
  • 规则三:如果有多个输入生命周期,但其中一个是 &self 或 &mut self,那么 self 的生命周期会赋值给所有的输出生命周期;

如果编译器在应用完上述三个规则后,能够计算出返回值的生命周期,则可以省略,否则不能省略。这些规则同样适用于 fn 定义和 impl 块,我们来举几个例子,感受一下整个过程。

// 函数如下,然后开始应用三个规则
fn first_word(s: &str) -> &str{};

// 1. 每个引用类型的参数都有自己的生命周期,满足
//    所以函数相当于变成如下
fn first_word<'a>(s: &'a str) -> &str{};

// 2. 只有一个输入生命周期,该生命周期被赋给所有的输出生命周期
//    显然也是满足的,所以函数变成如下
fn first_word<'a>(s: &'a str) -> &'a str{};

// 3. 不满足,所以无事发生

应用完三个规则之后,计算出了返回值的生命周期,所以合法。

再举个例子:

// 函数如下,然后开始应用三个规则
fn first_word(s1: &str, s2: &str) -> &str{};

// 1. 每个引用类型的参数都有自己的生命周期
//    显然满足,所以函数变成如下
fn first_word<'a, 'b>(s1: &'a str, s2: &'b str) -> &str{};

// 2. 只有一个输入生命周期,该生命周期被赋予所有的输出生命周期
// 但是这里有两个,所以不满足

// 3. 不满足

当编译器使用了 3 个规则之后仍然无法计算出返回值的生命周期时,就会出现编译错误,显然上面代码是会报错的。我们需要手动标注生命周期:

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

从表面上来看 x、y 的生命周期是相同的,都是 'a,但准确来说它表示的是 x、y 生命周期重叠的部分。而返回值的生命周期标注也是 'a,所以此处的含义就表示输出生命周期是两个输入生命周期重叠的部分。

longest 函数这么改的话,是合法的。

方法中的生命周期标注

然后是在方法中标注生命周期,它的语法和泛型是相似的。

// 声明周期的语法类似于泛型
// 必须要先通过 <'a> 进行声明,然后才能使用
struct Girl <'a> {
    name: &'a str,
}

// 在学习泛型的时候我们知道
// 这种方式表示为某个类型实现方法
// 现在则变成生命周期,并且 <'a> 不可以省略
impl <'a> Girl <'a> {
    fn say_hi(&self) -> String {
        String::from("hello world")
    }

    // 此处无需指定生命周期,因为 Rust 可以推断出来
    // 会自动将 self 的生命周期赋值给所有的输出生命周期
    fn get_name(&self, useless_arg: &str) -> &str {
        self.name
    }
}
fn main() {
    let name = String::from("古明地觉");
    let g = Girl{name:&name};

    println!("{}", g.say_hi());  // hello world
    println!("{}", g.get_name(""))  // 古明地觉
}

比较简单,另外程序中还有一个特殊的生命周期叫 'static,它表示整个程序的持续时间。所有的字符串字面量都拥有 'static 生命周期:

fn main() {
    let s: &'static str = "hello";
}

为引用指定 'static 之前需要三思,是否需要引用在整个程序的生命周期内都存活。

同时指定生命周期和泛型

生命周期的指定方式和泛型是一样的,那如果想同时指定生命周期和泛型,应该怎么做呢?

fn largest<'a, T>(x: &'a str, y: &'a str,
                  useless_arg: T) -> &'a str {
    if x > y {
        x
    } else {
        y
    }
}

fn main() {
    let s1 = "hello";
    let s2 = "hellO";
    println!("{}", largest(s1, s2, ""));
    // hello
}

非常简单,但要保证生命周期在前,泛型在后。

以上就是 Rust 的生命周期,它并没有改变 Rust 变量的存活时间,只是给了借用检查器更多的余地去推断引用是否合法。

就目前来说,我们介绍的内容都还很基础,应该很好理解。等把基础说完了,后面会介绍更多关于 Rust 的细节。最后的最后,我们再一起用 Rust 手写一个简易版的 Redis,并和现有的 Redis 做一下性能对比。

到此这篇关于深入了解Rust的生命周期的文章就介绍到这了,更多相关Rust 生命周期内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 解析Rust struct 中的生命周期

    最近在用rust 写一个redis的数据校验工具.redis-rs中具备 redis::ConnectionLike trait,借助它可以较好的来抽象校验过程.在开发中,不免要定义struct 中的某些元素为 trait object,从而带来一些rust语言中的生命周期问题.本文不具体讨论 redis的数据校验过程,通过一个简单的例子来聊聊 struct 中 trait object 元素的生命周期问题. 首先来定义一个 base trait,该 trait 中只包含一个函数,返回Strin

  • Rust指南之生命周期机制详解

    目录 前言 1.所有权中的垂悬引用解析 2.结构体中使用String 而不用&str 的原因 3.生命周期注释 4.结构体中使用字符串切片引用 5.静态生命周期 6.泛型.特性与生命周期综合使用 前言   Rust 生命周期机制是与所有权机制同等重要的资源管理机制,之所以引入这个概念主要是应对复杂类型系统中资源管理的问题.引用是对待复杂类型时必不可少的机制,毕竟在Rust 中复杂类型的数据不能被处理器轻易地复制和计算.但是为什么还有引入生命周期的概念呢,这是因为引用常常会导致非常复杂的资源管理问

  • 深入了解Rust的生命周期

    目录 楔子 生命周期标注语法 结构体中的生命周期标注 生命周期的省略 方法中的生命周期标注 同时指定生命周期和泛型 楔子 Rust 的每个引用都有自己的生命周期,生命周期指的是引用保持有效的作用域.大多数情况下,引用是隐式的.可以被推断出来的,但当引用可能以不同的方式互相关联时,则需要手动标注生命周期. fn main() {     let r;     {         let x = 5;         r = &x;     }  // 此处 r 不再有效     println!(

  • rust生命周期详解

    目录 rust生命周期 借用检查 函数中的生命周期 手动声明生命周期 结构体中的生命周期 生命周期消除 三条消除原则 生命周期约束 静态生命周期 rust生命周期 生命周期是rust中用来规定引用的有效作用域.在大多数时候,无需手动声明,因为编译器能够自动推导.当编译器无法自动推导出生命周期的时候,就需要我们手动标明生命周期.生命周期主要是为了避免悬垂引用. 借用检查 rust的编译器会使用借用检查器来检查我们程序的借用正确性.例如: #![allow(unused)] fn main() {

  • Angular2生命周期钩子函数的详细介绍

    Angular每个组件都存在一个生命周期,从创建,变更到销毁.Angular提供组件生命周期钩子,把这些关键时刻暴露出来,赋予在这些关键结点和组件进行交互的能力,掌握生命周期,可以让我们更好的开发Angular应用 概述 每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上ng前缀构成的.比如,OnInit接口的钩子方法叫做ngOnInit, Angular在创建组件后立刻调用它 生命周期执行顺序 ngOnChanges 在有输入属性的情况下才会调用,该方法接受当前和上一属性值的Simpl

  • 浅谈angular2 组件的生命周期钩子

    本文介绍了浅谈angular2 组件的生命周期钩子,分享给大家,具体如下: 按照生命周期执行的先后顺序,Angular生命周期接口如下所示 名称 时机 接口 范围 ngOnChanges 当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit之前. OnChanges 指令和组件 ngOnInit 在第一轮 ngOnChanges 完成之后调用. ( 译注:也就是说当每个输入属性的值都被触发了一次 ngOnChanges之后才会调用 ngOnInit ,此时所有输入属性都已

  • AngularJS深入探讨scope,继承结构,事件系统和生命周期

    本文实例讲述了AngularJS的scope,继承结构,事件系统和生命周期.分享给大家供大家参考,具体如下: 深入探讨 Scope 作用域 每一个 $scope 都是类 Scope 的一个实例.类 Scope 拥有可以控制 scope 生命周期的方法,提供事件传播的能力,并支持模板渲染. 作用域的层次结构 让我们再来看看这个简单的 HelloCtrl 的例子: var HelloCtrl = function($scope){ $scope.name = 'World'; } HelloCtrl

  • 浅谈Spring bean 生命周期验证

    一.从源码注释看bean生命周期 从JDK源码上看,BeanFactory实现类需要支持Bean的完整生命周期,完整的初始化方法及其标准顺序(格式:接口 方法)为: 1.BeanNameAware setBeanName 设置bean名称 2.BeanClassLoaderAware setBeanClassLoader 设置bean类加载器 3.BeanFactoryAware setBeanFactory 设置bean工厂 4.EnvironmentAware setEnvironment

  • Spring配置使用之Bean生命周期详解

    基本概念 Spring 中的 Bean 的生命周期,指的是 Bean 从创建到销毁的过程. 下面来探究下几个有关 Bean 生命周期配置的属性. lazy-init lazy-init 表示延迟加载 Bean,默认在 Spring IoC 容器初始化时会实例化所有在配置文件定义的 Bean,若启用了 lazy-init 则在调用 Bean 时才会去创建 Bean. 定义 Bean: public class Animals { public Animals(){ System.out.print

  • IOS UIView的生命周期的实例详解

    IOS UIView的生命周期的实例详解 任何对象的者有一个生命周期,即都存在一个实例化到销毁的过程. UIView对象也不例外,那么UIView从init/new开始后,直到dealloc结束的过程中都经历了哪些过程呢? 首先自定义继承自UIView的对象LifeView #import <UIKit/UIKit.h> @interface LifeView : UIView @end #import "LifeView.h" @interface LifeView ()

随机推荐