对Rust所有权、借用及生命周期的理解 rust 生命周期

   2023-02-09 学习力0
核心提示:Rust的内存管理中涉及所有权、借用与生命周期这三个概念,下面是个人的一点粗浅理解。一、从内存安全的角度理解Rust中的所有权、借用、生命周期要理解这三个概念,你首要想的是这么做的出发点是什么——内存安全,这是Rust非常强调的一点。可以这么理解,所有

Rust的内存管理中涉及所有权、借用与生命周期这三个概念,下面是个人的一点粗浅理解。

一、从内存安全的角度理解Rust中的所有权、借用、生命周期

要理解这三个概念,你首要想的是这么做的出发点是什么——内存安全,这是Rust非常强调的一点。可以这么理解,所有权、借用与生命周期很大程度上是为内存安全而设计的。

所有权,从内存安全的角度思考,如果一个实例有多个所有者,这个实例就很可能不安全,多个所有者都可能操作这个实例产生竞争,解决的办法是让他只有一个所有者,这样就无论如何也无法产生竞争(Data race)。新问题来了,如果其他人想访问这个实例怎么办?借用。

借用,有点类似与引用,可以理解为我不去获取这个实例的所有权,我只借用一下,用完后就还回去,只使用而不占有。有两种借用,可变借用与不可变借用。后面有规则说明。

生命周期,对生命周期的理解可以暂时这么认为:它的目的是避免悬垂指针的出现或者是保证引用的有效性。 如果你进行一个借用,而那个被借用的实例超出作用域或者已经被释放了(即你的生命周期比你借用的实例的生命周期长),此时,你指向的是一个无意义的地址,可能会因此产生严重错误,而且这种错误又难发现,你必须对所借用的实例的生命周期非常清晰,才不会产生错误。在C++中,没有显式的生命周期概念,程序员在处理这种情况时必须十分小心且需对程序十分清晰,否则就有可能出现悬垂指针。在Rust中,通过生命周期,明确指出各个对象的生命周期,以保证你的生命周期与你借用的实例的生命周期处在一个交集中,你的生命周期不会超过你借用的实例的生命周期。尽管在Rust中也比较难以处理,但是Rust编译器会显示的指明你可能产生的生命周期错误,强制你处理生命周期问题,从而避免可能引发的错误。

可以说,编写Rust程序,编译器会根据所有权、借用、生命周等规则对代码进行检查,如果不合乎Rust的规则,就不会编译通过(尽管你认为这样的代码目前没有Bug,但问题是编译器认为这样有可能产生Bug),减少了未来程序出现内存问题的几率。

二、所有权

一般程序管理计算机内存的方式主要有两种,一种是垃圾回收机制(GC),一种是程序员自己分配和释放内存,这两种方式各自优缺点暂且不谈,Rust采用的是第三种方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

所有权规则:

  1. Rust 中的每一个值都有一个被称为其所有者(owner)的变量。
  2. 值有且只有一个所有者。
  3. 当所有者(变量)离开作用域,这个值将被丢弃。
{
    let s = String::from("hello"); // 从此处起,s 是有效的

    // 使用 s
}                                  // 此作用域已结束,
                                   // s 不再有效

Rust中内存在拥有它的变量离开作用域后就被自动释放。编译器可以识别出变量的生命周期,在编译时就知道何时释放该变量占用的内存。

三、借用&引用

借用与引用是相对比较好理解的,为了保证内存安全,Rust制定了如下的引用规则。强制在编译期间进行如下的规则检查,

引用规则

  1. 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用。
  2. 引用必须总是有效。

这2条规则的核心要点是要避数据竞争和悬垂指针的出现。

下面用C++和Rust的代码做对比,可以看出Rust编译器对代码进行严格的安全检查,对有可能出现问题的代码显示提出编译错误,尽管代码可能并不会产生数据竞争或悬垂指针。

C++代码编译通过,没有产生编译错误:

#include <iostream>
using namespace std;

int main(){
  int a = 10;
  int &b = a;
  a = 100;
  b = 200;

  cout<<a<<endl;
  cout<<b<<endl;

  return 0;
}

//编译通过

Rust代码编译不通过,尽管代码目前看起来不会产生可能的错误,但编译器进行严格的检查,编译不通过:

fn main() {
    let mut a = 10;
    let ref b = a;
    let ref mut c = a;
    //编译报错:cannot borrow `a` as immutable because it is also borrowed as mutable
}

四、生命周期

生命周期(lifetimes),它是一类允许我们向编译器提供引用如何相互关联的泛型。Rust 的生命周期功能允许在很多场景下借用值的同时仍然使编译器能够检查这些引用的有效性。

【1】c++与rust的不同

生命周期的主要目标是避免悬垂引用,它会导致程序引用了非预期引用的数据。Rust编译器会强制进行生命周期的检查,如果不符合规则,就编译报错,强制你编写符合生命周期规则的代码。下面对比Rust和C++的代码说明Rust增加生命周期的动机与好处。

Rust代码,如果不符合生命周期规则,就编译错误。

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

编译产生如下错误:

error[E0597]: `x` does not live long enough

  --> src/main.rs:76:18
   |
76 |             r = &x;
   |                  ^ borrowed value does not live long enough
77 |         }
   |         - `x` dropped here while still borrowed
...
80 |     }
   |     - borrowed value needs to live until here

C++代码,即使会产生悬垂指针,编译还是会通过,并且正常运行,这样悬垂指针可能会导致非常隐晦的bug,造成排查bug的困难。

#include <iostream>
using namespace std;

int main(){
  int *r = new int(5);
  int *x = r;
  delete r;

  cout<< *r <<endl;
  cout<< *x <<endl;

  return 0;
}

输出结果:

0
2608    //这种情况可能会产生较为隐晦的Bug

编译通过,但有可能会产生Bug,也有可能不会引发Bug,但是一旦由此产生Bug,可能比较难找。

通过上面的代码,我们可以看到增加生命周期对内存安全的好处。一定程度上,生命周期是Rust强调内存安全的产物。下面再次通过代码说明为什么需要生命周期。

【2】为什么需要生命周期?

简单的理解是当编译器判断不出引用的有效性,不能够判断出引用内存是否安全的时候,就需要程序员通过在Rust代码中明确的生命周期注解为编译器指明每个引用的生命周期,告诉编译器足够的信息,使编译器能够判断这段引用是否有效,符不符合规则要求,不会出现垂悬引用这种不安全的操作。

下面这段代码会产生垂悬引用:

fn main() {
    let a = 10;
    let m;
    {
        let b = 100;

        m = max_num(&a, &b);
        assert_eq!(100, *m);
    }
    println!("max num is {}", m);
}

fn max_num(a: &i32, b: &i32) -> &i32 {
    if *a > *b {
        return *a;
    }
    *b
}

在Rust中,上面的代码会产生编译错误:

error[E0106]: missing lifetime specifier
  --> src/main.rs:23:33
   |
23 | fn max_num(a: &i32, b: &i32) -> &i32 {
   |                                 ^ expected lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does
 not say whether it is borrowed from `a` or `b`

编译器提示需要添加生命周期注解,因为编译器目前不能判断最后是返回a的引用还是b的。所以编译器无法通过作用域来确定返回的引用是否总是有效。如果返回的是a的引用,那么上面的不会产生垂悬引用,如果返回的是b的引用,那么就会产生垂悬引用,产生安全隐患。

怎么办呢?通过增加生命周期注解告诉编译器更多的信息,使它能够判断出引用是否有效。

// 增加生命周期注解
fn max_num<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
    if *a > *b {
        return a;
    }
    b
}

通过增加生命周期注解,编译器就能够判断引用的有效性:

error[E0597]: `b` does not live long enough
  --> src/main.rs:16:26
   |
16 |         m = max_num(&a, &b);
   |                          ^ borrowed value does not live long enough
17 |         assert_eq!(100, *m);
18 |     }

   |     - `b` dropped here while still borrowed
19 |     println!("max num is {}", m);
20 | }
   | - borrowed value needs to live until here

编译器告诉我们,b不满足生命周期注解的要求,因为我们的生命周期注解中说明了返回的引用的生命周期在a和b中较短的那个生命周期结束之前保持有效。具体分析,这里m的生命周期是a和b生命周期的交集。

根据编译器提示信息,更改为以下代码,编译成功:

fn main() {
    // let inner = Inner{data:"inner data."};
    let a = 1000;
    let b = 100;    //将b的生命周期延长至与a,m相同
    let m;
    {
        m = max_num(&a, &b);
        assert_eq!(100, *m);
    }
    println!("max num is {}", m);
}
【3】生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短。注解语法如下示例:

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

关注微信公众号,获取更多Rust文章!
对Rust所有权、借用及生命周期的理解

 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • bloom-server 基于 rust 编写的 rest api cache 中间件
    bloom-server 基于 rust 编写的 rest api cache
    bloom-server 基于 rust 编写的 rest api cache 中间件,他位于lb 与api worker 之间,使用redis 作为缓存内容存储, 我们需要做的就是配置proxy,同时他使用基于share 的概念,进行cache 的分布存储,包含了请求端口(proxy,访问数据) 以及cache 控制端口(
    03-08
  • #新闻拍一拍# Oracle 调研如何避免让 Java 开发者投奔 Rust 和 Kotlin | Linux 中国
    #新闻拍一拍# Oracle 调研如何避免让 Java 开发
     导读:• 英特尔对迟迟不被 Linux 主线接受的 SGX Enclave 进行了第 38 次修订 • ARM 支持开源的 Panfrost Gallium3D 驱动本文字数:977,阅读时长大约:1分钟作者:硬核老王Oracle 调研如何避免让 Java 开发者投奔 Rust 和 KotlinOracle 委托分析公司 Omd
    03-08
  • Linux系统下Rust快速安装:国内镜像加速
    Linux系统下Rust快速安装:国内镜像加速
    官方网址和方法Install Rust - Rust Programming Language然而速度慢得让人难以置信。利用国内镜像进行windows的Linux子系统的Rust安装。rust 使用国内镜像,快速安装方法参考:RUST安装慢怎么办,使用镜像方式安装_网络_为中华之崛起而编程-CSDN博客我的操作
    03-08
  • Rust到底值不值得学--Rust对比、特色和理念
    前言其实我一直弄不明白一点,那就是计算机技术的发展,是让这个世界变得简单了,还是变得更复杂了。当然这只是一个玩笑,可别把这个问题当真。然而对于IT从业者来说,这可不是一个玩笑。几乎每一次的技术发展,都让这个生态变得更为复杂。“英年早秃”已经成
    03-08
  • 超33000行新代码,为Linux内核添加Rust支持的补丁已准备就绪
    超33000行新代码,为Linux内核添加Rust支持的补
    https://mp.weixin.qq.com/s/oKw9aBJSdmRoO6-rbLAkNw7 月 4 日,一套修订后的补丁被提交至 Linux 内核的邮件列表中,该补丁为在 Linux 内核中以 Rust 作为辅助编程语言提供了支持,借助 Rust 可以提高 Linux 内核和内存的安全。整套补丁包含 17 个子项,不光
    03-08
  • 【译】Rust 的 Result 类型入门
    【译】Rust 的 Result 类型入门
    A Primer on Rust’s Result Type 译文原文链接:https://medium.com/@JoeKreydt/a-primer-on-rusts-result-type-66363cf18e6a原文作者:Joe Kreydt译文出处:https://github.com/suhanyujie/article-transfer-rs译者:suhanyujietips:水平有限,翻译不当之
    03-08
  • Rust实战系列-基本语法
    Rust实战系列-基本语法
    主要介绍 Rust 的语法、基本类型和数据结构,通过实现一个简单版 grep 命令行工具,来理解 Rust 独有的特性。本文是《Rust in action》学习总结系列的第二部分,更多内容请看已发布文章:一、Rust实战系列-Rust介绍“主要介绍 Rust 的语法、基本类型和数据结
    03-08
  • 全栈程序员的新玩具Rust(三)板条箱
    上次用到了stdout,这次我们来写一个更复杂一点的游戏rust的标准库叫做std,默认就会引入。这次我们要用到一个随机数函数,而随机数比较尴尬的一点是这玩意不在标准库中,我们要额外依赖一个库。很多编程方案都有自己的模块化库系统,rust也不例外,不过rust
    02-10
  • 全栈程序员的新玩具Rust(六)第一个WASM程序
    全栈程序员的新玩具Rust(六)第一个WASM程序
    先上代码https://gitee.com/lightsever/rust_study/tree/master/wasm_hello01webassembly就不用再赘述了,耳朵里面快磨出茧子来了。rustwasm是火狐自家的玩具,让我们来继续做实验,让rust飞起来吧。环境安装安装好rust环境之后仍然需要 一个 wasm 工具包carg
    02-10
  • 【Rust】标准库-Result rust数据库
    环境Rust 1.56.1VSCode 1.61.2概念参考:https://doc.rust-lang.org/stable/rust-by-example/std/result.html示例main.rsmod checked {#[derive(Debug)]pub enum MathError {DivisionByZero,NonPositiveLogarithm,NegativeSquareRoot,}pub type MathResult =
    02-09
点击排行