首页 > 其他分享 >Rust——生命周期

Rust——生命周期

时间:2023-07-31 13:45:34浏览次数:41  
标签:生命周期 let Rust str println fn 引用

简而言之,即引用的有效作用域;一般情况下编译器会自动检查推导,但是当多个声明周期存在时,编译器无法推导出某个引用的生命周期,需要手动标明生命周期。

悬垂指针

悬垂指针是指一个指针指向了被释放的内存或者没有被初始化的内存。当尝试使用一个悬垂指针时,无法保证内存中的数据是否有效,从而导致程序的不确定行为。

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

问题:r 引用了内部花括号中的 x 变量,但是 x 会在内部花括号 } 处被释放,因此回到外部花括号后,r 会引用一个无效的 x。此时 r 就是一个悬垂指针,它引用了提前被释放的变量 x。生命周期就是为了解决这种问题的出现。

借用检查

Rust 使用借用检查器检查程序的借用正确性。

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

r 变量被赋予了生命周期 'a ,x 被赋予了生命周期 'b 。在编译期,Rust 会比较两个变量的生命周期,发现 r 拥有生命周期 'a,但是却引用了一个小得多的生命周期 'b,在这种情况下,编译器会认为程序存在风险拒绝运行。

如果想编译通过,需要使 x 变了活得比 r 久

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

生命周期

定义及语法

Rust 生命周期通过引用和所有权机制来管理内存的使用。每个变量都有其所有权,只有所有者可以释放其对应的内存。当变量被移动或引用时,其所有权也会随之转移。

需要注意的是,生命周期标注不会改变引用的实际作用域。编译器无法确定生命周期时,需要人工明确指定变量和引用的生命周期,以确保它们的使用是安全和有效的。

生命周期的语法:以 ' 开头,名称常用一个单独的小写字母如 'a 。

fn useless<'a>(first: &'a i32, second: &'a i32) {}

如果是引用类型的参数,那么生命周期会位于引用符号 & 之后,并用一个空格来将生命周期和引用参数分隔开:

&i32        // 一个引用
&'a i32     // 具有显式生命周期的引用
&'a mut i32 // 具有显式生命周期的可变引用

函数中的生命周期

原始函数如下

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(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

运行时会抛出异常

error[E0106]: missing lifetime specifier
 --> main.rs:8:33
  |
8 | 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
  |
8 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

error: aborting due to previous error

由异常可以看到,编译器无法确定 longest 函数的返回值是引用 x 还是引用 y 来进行后续的引用生命周期分析。即在存在多个引用时,编译器有时会无法自动推导生命周期

对 longest 函数添加生命周期标注

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

  • 和泛型一样,使用生命周期参数,需要先声明 <'a>
  • x、y 和返回值至少活得和 'a 一样久

需要强调的是,在通过函数签名指定生命周期参数时没有改变传入引用或者返回引用的真实生命周期,而是告诉编译器当不满足此约束条件时,就拒绝编译通过

当把具体的引用传给 longest 时,那生命周期 'a 的大小就是 x 和 y 的作用域的重合部分,即'a 大小为 x 和 y 中较小的那个。

如下所示

fn main() {
    let string1 = String::from("long string is long");
    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

此时,string2 的生命周期比 string1 小,因此 result 和 string2 活的一样久。如果对代码进行修改

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());
    }
    println!("The longest string is {}", result);
}

此时由于生命周期 result = string1 > string2,因此会抛出异常

 --> main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());   
  |                                            ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

error: aborting due to previous error

特殊场景

只返回第一个参数

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

y 完全没有被使用,即不需要为 y 标注生命周期,只需要标注 x 参数和返回值即可。

返回值为引用

若返回值为引用类型,则生命周期为

  • 函数参数的生命周期
  • 函数体中某个新建引用的生命周期

若为后者有可能出现悬垂指针:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

此时抛出异常

error[E0515]: cannot return reference to local variable `result`
  --> main.rs:15:5
   |
15 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

result 在函数结束后就被释放,但是仍然存在对 result 的引用,无法指定合适的生命周期(好处是避免了悬垂引用),为了解决这种异常,可以返回内部字符串的所有权,将字符串的所有权转移给调用者。

fn longest<'a>(_x: &str, _y: &str) -> String { //此处直接返回的是String,而不是引用类型
    String::from("really long string")
}
fn main() {
   let s = longest("not", "important");
   println!("{}",s)
}

结构体中的生命周期

在结构体的字段中存在引用时,需要为结构体的每个引用标注生命周期。

#[allow(dead_code)]
#[derive(Debug)]
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,
    };
    println!("{:?}",i);
}

结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <'a>。该生命周期标注说明,结构体 ImportantExcerpt 所引用的字符串 str 必须比该结构体活得更久。

#[allow(dead_code)]
#[derive(Debug)]
struct ImportantExcerpt<'a> {
    part: &'a str,
}
fn main() {
    let i; 
    {
        let novel = String::from("Call me Ishmael. Some years ago...");
        let first_sentence = novel.split('.').next().expect("Could not find a '.'");
        i = ImportantExcerpt {
            part: first_sentence,
        };
    }
    println!("{:?}",i); // 此处使用了结构体,而里面引用的字符串
}

引用字符串在内部语句块末尾 } 被释放后,println! 依然在外面使用了该结构体,因此会导致无效的引用

error[E0597]: `novel` does not live long enough
  --> main.rs:10:30
   |
9  |         let novel = String::from("Call me Ishmael. Some years ago...");
   |             ----- binding `novel` declared here
10 |         let first_sentence = novel.split('.').next().expect("Could not find a '.'");
   |                              ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
...
14 |     }
   |     - `novel` dropped here while still borrowed
15 |     println!("{:?}",i);
   |                     - borrow later used here

生命周期的消除规则

对于编译器来说,每一个引用类型都有一个生命周期,但是在编写程序时无需标注

fn first_word(s: &str) -> &str { //参数和返回值都为引用,没有标注生命周期仍然可以通过
    let bytes = s.as_bytes();
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

编译器为了简化用户的使用,运用了生命周期消除。对于 first_word 函数,它的返回值是一个引用类型,那么该引用只有两种情况:

  • 从参数获取
  • 从函数体内部新创建的变量获取

当返回值的引用是来自参数时,说明参数和返回值的生命周期是一样的。因此不标注也不会产生歧义。

消除规则

注意点:

  • 若编译器不能确定某件事是正确时,会直接判为不正确,此时需要手动标注生命周期
  • 函数或者方法中,参数的生命周期被称为输入生命周期,返回值的生命周期被称为输出生命周期

三条消除规则

  • 每一个引用参数都会获得独自的生命周期(输入)
  • 若只有一个输入生命周期(函数参数中只有一个引用类型),该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期(输出)
  • 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期(输出)
    注:存在&self 说明该函数是一个方法。如果返回值生命周期和 &self 不一样需要手动标注。

方法中的生命周期

为具有生命周期的结构体实现方法时,使用的语法跟泛型参数语法很相似

struct ImportantExcerpt<'a> {
    part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

注意点:

  • impl 中必须使用结构体的完整名称,包括 <'a>,因为生命周期标注也是结构体类型的一部分
  • 方法签名中,由于生命周期消除的第一和第三规则一般不需要标注生命周期

编译器应用规则步骤:
原始代码

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

编译器应用第一规则,给予每个输入参数一个生命周期

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &str { //不知道announcement生命周期,因此重新赋予生命周期b
        println!("Attention please: {}", announcement);
        self.part
    }
}

编译器应用第三规则,将 &self 的生命周期赋给返回值 &str

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

如果手动标注返回值的生命周期为 'b,此时会报错,这是由于编译器无法知道 'a 和 'b 的关系。由于 &'a self 是被引用的一方,因此引用它的 &'b str 必须要活得比它短,否则会出现悬垂引用,因此需要使生命周期 'b 必须要比 'a 小。

方法 1:生命周期约束语法'a: 'b,说明'a 必须比 'b 活得久;把 'a 和 'b 都在同一个地方声明

impl<'a: 'b, 'b> ImportantExcerpt<'a> {
    fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

方法 2:分开声明但通过 where 'a: 'b 约束生命周期关系

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
    where
        'a: 'b,
    {
        println!("Attention please: {}", announcement);
        self.part
    }
}

静态生命周期

静态生命周期'static,拥有该生命周期的引用可以和整个程序活得一样久。

let s: &'static str = "long static";

可以帮助解决非常复杂的生命周期问题甚至是无法被手动解决的生命周期问题

'static,有两种用法: &'static 和 T: 'static

&'static

一个引用必须要活得跟剩下的程序一样久,才能被标注为 &'static。主要注意的是, &'static 生命周期针对的仅仅是引用,而不是持有该引用的变量,对于变量来说,还是要遵循相应的作用域规则:

use std::{slice::from_raw_parts, str::from_utf8_unchecked};

fn get_memory_location() -> (usize, usize) {
  // “Hello World” 是字符串字面量,因此它的生命周期是 `'static`.
  // 但持有它的变量 `string` 的生命周期就不一样了,它完全取决于变量作用域,对于该例子来说,也就是当前的函数范围
  let string = "Hello World!";
  let pointer = string.as_ptr() as usize;
  let length = string.len();
  (pointer, length)
  // `string` 在这里被 drop 释放
  // 虽然变量被释放,无法再被访问,但是数据依然还会继续存活
}

fn get_str_at_location(pointer: usize, length: usize) -> &'static str {
  // 使用裸指针需要 `unsafe{}` 语句块
  unsafe { from_utf8_unchecked(from_raw_parts(pointer as *const u8, length)) }
}

fn main() {
  let (pointer, length) = get_memory_location();
  let message = get_str_at_location(pointer, length);
  println!(
    "The {} bytes at 0x{:X} stored: {}",
    length, pointer, message //The 12 bytes at 0x563D1B68305B stored: Hello World!
  );
}

可以看到持有 &'static 引用的变量的生命周期受到作用域的限制

T: 'static

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: T) {
    println!( "'static value passed in is: {:?}", input );
}

fn print_it1( input: impl Debug + 'static ) {
    println!( "'static value passed in is: {:?}", input );
}

fn main() {
    let i = 5;
    print_it(&i);
    print_it1(&i);
}

上述两种形式的代码中,T: 'static 与 &'static 有相同的约束:T 必须活得和程序一样久;该代码会报错,&i 的生命周期无法满足 'static 的约束

修改方法一:将 i 修改为常量

fn main() {
    const I: i32 = 5;
    print_it(&I);
}

修改方法二: 将 T 修改为 &T

use std::fmt::Debug;

fn print_it<T: Debug + 'static>( input: &T) {
    println!( "'static value passed in is: {:?}", input );
}

fn main() {
    let i = 5;
    print_it(&i);
}

原因在于约束的是 T,但是使用是它的引用 &T,即没有直接使用 T,因此不会检查 T 的生命周期约束。

总而言之,&'static 引用指向的数据活得跟程序一样久,引用本身是要遵循其作用域范围的。

标签:生命周期,let,Rust,str,println,fn,引用
From: https://www.cnblogs.com/ConfusedChenSir/p/17593214.html

相关文章

  • Activity及其生命周期
    Activity是Android用户界面的基础组件,它一般存放在任务栈(Task)中, 所以Activity是以栈的形式存放的,也就遵循先进后出的原则,也不支持重新排序。如果要改变Activtiy的顺序,只能根据压栈和出栈的操作来改变。当启动一个Application时,系统会默认创建一个对应的Task,用来存放......
  • 线程的生命周期
    线程的生命周期1、线程的5个生命周期新建:刚使用new方法创建出来的线程;就绪:调用线程的start()方法后,线程处于等待CPU分配资源阶段,当线程获取到CPU资源后开始执行;运行:当就绪的线程被调度并获得CPU资源时,便会进入运行状态,run()方法定义了线程的操作和功能;阻塞:在运行状态的时候,可能......
  • - 通过结合前端页面实现ORM对数据的增删改查 - Django中如何创建表关系 - 一对一
    通过结合前端页面实现ORM对数据的增删改查案例:写一个页面,就是把数据库中的数据以表格的形式展示出来,然后在每一行的后面加两个按钮,分别是修改、删除的按钮1.首先在数据库创建一个表格1.在model.py中创建表格 2.pythonmanage.pymakemigratins迁移记录   3.......
  • 5_Spring_Bean的生命周期
    5_Spring_Bean的生命周期bean从创建到销毁经历的各个阶段以及每个阶段所调用的方法1通过构造器创建bean实例     执行构造器2为bean属性赋值            执行set方法3初始化bean                调......
  • Rust随笔——结构体打印和所有权转移
    结构体打印如果想打印结构体,并不能使用如以下方式进行打印println!("{}",rectangle);会出现上图所示的错误,通过阅读不难得出——报错原因为Rect类型没有实现std::fmt::Display这个trait。第一个note建议我们使用{:?}或{:#?}来代替{}进行输出,于是尝试修改后进行构建修改后,......
  • ORM对数据的增删改查,动静态网页,Django创建表关系,Django框架的请求生命周期流程图
    通过结合前端页面实现ORM对数据的增删改查#我让你写一个页面,就是把数据库中的数据以表格的形式展示出来,然后在每一行的后面加两个按钮,分别是修改、删除的按钮#表格的展示页面'''思考修改功能的逻辑:'''1、确定修改哪条记录,怎么确定?通过主键id确定唯一一条记录2、点击......
  • Vue2 & Vue3生命周期对比
    Vue2生命周期图示  Vue3生命周期图示  二者对比 ......
  • 《一个程序猿的生命周期》-《发展篇》- 44.再次进军内蒙市场(转型)
        2015年9月16日写过一篇文章《争取内蒙区的市场销售》,这是发生在2013年左右的事,距今相隔10年左右的时间。2023年1月18日(春节前)开始几乎每周都要去内蒙,从第一次与稀土领域的领导交流,中间出现新的稀土项目机会,现场交流、商务、招人、调研、方案设计等,即有身心疲惫又有努......
  • NET6 EF Error: The certificate chain was issued by an authority that is not trus
    ErrorAconnectionwassuccessfullyestablishedwiththeserver,butthenanerroroccurredduringtheloginprocess.(provider:SSLProvider,error:0-Thecertificatechainwasissuedbyanauthoritythatisnottrusted.)解决方法:在DB连接字符串后面添加......
  • 用 Rust 生成 Ant-Design Table Columns
    经常开发表格,是不是已经被手写Ant-DesignTable的Columns整烦了?尤其是ToB项目,表格经常动不动就几十列。每次照着后端给的接口文档一个个配置,太头疼了,主要是有时还会粘错就尴尬了。那有没有办法能自动生成columns配置呢?当然可以。目前后端的接口文档一般是使用Swagger来生成的,S......