Rust - 引用和借用

上一篇章末尾提到,如果仅仅支持通过转移所有权的方式获取一个值,那会让程序变得复杂。 Rust 能否像其它编程语言一样,使用某个变量的指针或者引用呢?答案是可以。

Rust 通过 借用(Borrowing) 这个行为来达成上述的目的,获取变量的引用操作,称之为借用(borrowing)
正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

(一)引用与解引用

常规引用是一个指针类型,指向了对象存储的内存地址。在下面代码中,我们创建一个 i32 值的引用 y,然后使用解引用运算符“ * ”来解出 y 所使用的值:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

assert_eq! :是一个“断言宏”,可以用于判断两个表达式返回的值是否相等。当不相等时,当前程序会直接报错。

变量 x 存放了一个 i32 值 5。y 是 x 的一个引用。可以直接断言判断 x 等于 5。

然而,如果希望对 y 的值做出断言判断,必须使用 *y 来解出引用所指向的值(也就是解引用)。一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。

相反如果不进行解引用,而直接编写“assert_eq!(5, y); ”,则会得到如下编译错误:

error[E0277]: can't compare `{integer}` with `&{integer}`
assert_eq!(5, y);
^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` // 无法比较整数类型和引用类型

不允许比较整数与引用,因为它们是不同的类型。必须使用解引用运算符解出引用所指向的值。

(二)不可变引用

下面的代码,我们用 s1 的引用作为参数传递给 calculate_length 函数,而不是把 s1 的所有权转移给该函数:

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1); //将引用传递给函数,此时函数获取到了值而没有拿到所有权

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len() //函数返回字符串长度值
}

能注意到两点:

  1. 无需像上章一样:先通过函数参数传入所有权,然后再通过函数返回来传出所有权,代码更加简洁
  2. calculate_length 的参数 s 类型从 String 变为 &String

这里,& 符号即是引用,它们允许你使用值,但是不让获取所有权,如图所示: image.png
通过 &s1 语法,我们创建了一个指向 s1 的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域后,其指向的值也不会被丢弃。

同理,函数 calculate_length 使用 & 来表明参数 s 的类型是一个引用:

fn calculate_length(s: &String) -> usize { // s 是对 String 的引用
    s.len()
} // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,所以什么也不会发生

一个形象的例子:我们进行了借用的行为,在理论上我们拿到的只是一个值。在此前提下,如果尝试修改借用的变量呢?

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

果然,这种修改是不被允许的:

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
    
fn change(some_string: &String) {
    ------- help: consider changing this to be a mutable reference: `&mut String`
    //------- 帮助:考虑将该参数类型修改为可变的引用: `&mut String`
some_string.push_str(", world");
    ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
    //some_string`是一个`&`类型的引用,因此它指向的数据无法进行修改

正如变量默认不可变一样,引用指向的值默认也是不可变的。

如果我们要对其进行修改,那么只需要进行一个小调整,即可解决这个问题。

(三)可变引用

只需要一个小调整,即可修复上面代码的错误:

fn main() {
    let mut s = String::from("hello"); //将s设置为可变

    change(&mut s); //传递引用时使其可变
}

fn change(some_string: &mut String) { //相应的,参数为可变
    some_string.push_str(", world");
}

首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

1. 注意:可变引用同时只能存在一个

不过可变引用并不是随心所欲、想用就用的,它有一个很大的限制:同一作用域,特定数据在每个时刻中只能有一个可变引用存在

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

以上代码会报错,错误信息如下:

error[E0499]: cannot borrow `s` as mutable more than once at a time 同一时间无法对 `s` 进行两次可变借用
  |
3 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here 首个可变引用在这里借用
4 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here 第二个可变引用在这里借用
6 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here 第一个借用在这里使用

这段代码出错的原因在于:第一个对 s 的进行可变借用的 r1 必须要持续到最后一次使用的位置 println!,在 r1 创建和最后一次使用之间,我们又创建了第二个可变借用 r2。

这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:

  • 两个或更多的指针同时访问同一数据
  • 至少有一个指针被用来写入数据
  • 没有同步数据访问的机制

数据竞争会导致发生不可预知的行为,这种行为难以在运行时追踪,并且难以诊断和修复。

而 Rust 它不会编译存在数据竞争的代码,所以避免了这种情况的发生。

很多时候,大括号可以帮我们解决一些编译不通过的问题,通过手动限制变量的作用域:

let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用

let r2 = &mut s;
2. 注意:可变引用与不可变引用不能同时存在

下面的代码会导致一个错误:

let mut s = String::from("hello");


let r1 = &s; // 没问题
let r2 = &s; // 没问题
let r3 = &mut s; // 大问题

println!("{}, {}, and {}", r1, r2, r3);

错误如下:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
        // 无法借用可变 `s` 因为它已经被借用了不可变
  |
4 |     let r1 = &s; // 没问题
  |              -- immutable borrow occurs here 不可变借用发生在这里
5 |     let r2 = &s; // 没问题
6 |     let r3 = &mut s; // 大问题
  |              ^^^^^^ mutable borrow occurs here 可变借用发生在这里
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here 不可变借用在这里使用

其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。

“对于一个数据(变量),可以同时存在多个对它的不可变引用” 是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。

注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同。变量的作用域是从创建的位置向下持续到作用域的关闭花括号“ } ”

Rust 的编译器一直在优化,早期的时候,引用的作用域跟变量作用域是一致的,这对日常使用带来了很大的困扰,你必须非常小心的去安排可变、不可变变量的借用,免得无法通过编译,例如以下代码:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
} // 老编译器中,r1、r2、r3作用域在这里结束
// 新编译器中,r3作用域在这里结束

在老版本的编译器中(Rust 1.31 前),将会报错,因为 r1 和 r2 的作用域在花括号 } 处结束,那么 r3 的借用就会触发 无法同时借用可变和不可变的规则。

但是在新的编译器中,该代码将顺利通过,因为Rust新规定了:“引用作用域的结束位置从花括号变成最后一次使用的位置”,因此 r1 借用和 r2 借用在 println! 后,就结束了,此时 r3 可以顺利借用到可变引用。

所以便对应了开头提到的“你可以从他那里借来,当使用完毕后,也必须要物归原主。

对于这种编译器优化行为,Rust 专门起了一个名字 —— Non-Lexical Lifetimes(NLL),专门用于找到某个引用在作用域 ( } ) 结束前就不再被使用的代码位置。

虽然这种借用错误有的时候会让我们很郁闷,但其实也是 Rust 提前发现了潜在的 BUG,即使减慢了开发速度,但是从长期来看却大幅减少了后续开发和运维成本。

(四)悬垂引用

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,当这个值被释放掉时,指针仍然存在,但其指向的内存可能不存在任何值或已被其它变量重新使用。

在 Rust 中编译器中,可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止(结束)其引用的使用。

让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误:

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

这里是错误:

error[E0106]: missing lifetime specifier
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

错误信息引用了一个我们还未学习的概念:lifetime(生命周期)。不过,即使不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:

this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
该函数返回了一个借用的值,但是已经找不到它所借用值的来源

仔细看看 dangle 代码的每一步到底发生了什么:

fn dangle() -> &String { // dangle 返回一个字符串的引用

    let s = String::from("hello"); // s 是一个新字符串

    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
// 危险!

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!

其中一个很好的解决方法是直接返回 String:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

这样就没有任何错误了,最终 String 的 所有权被转移给外面的调用者


引用的规则

让我们概括一下对引用的讨论:

  • 在任何时刻,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/567875.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入探索GDB:Linux下强大的调试神器

目录 一、GDB简介:源码级调试的基石 二、GDB基础操作:从入门到熟练 启动与基本命令 三、GDB进阶功能:解锁更深层次的调试能力 1. 回溯追踪:洞察调用栈 2. 动态内存检测:揪出内存问题 3. 条件断点与观察点&#…

JavaSE——程序逻辑控制

1. 顺序结构 顺序结构 比较简单,按照代码书写的顺序一行一行执行。 例如: public static void main(String[] args) {System.out.println(111);System.out.println(222);System.out.println(333);} 运行结果如下: 如果调整代码的书写顺序 , …

C++:继承作业题

1. 关于以下菱形继承说法不正确的是( ) class B {public: int b;};class C1: public B {public: int c1;};class C2: public B {public: int c2;};class D : public C1, public C2 {public: int d;};A.D总共占了20个字节B.B中的内容总共在D…

PE文件格式

PE文件格式 PE头:DOS头DOS存根NT头NT头:文件头NT头:可选头 节区头.text(代码)(节区头).data(数据)(节区头).rdata.idata,导入表 最后给出一个PE文件的16进制编辑器中的截图,找到其中每一个头的信息,和导入表…

2015NOIP普及组真题 3. 求和

线上OJ: 一本通:http://ybt.ssoier.cn:8088/problem_show.php?pid1971 核心思想: 本题的约束条件有两个: 条件1、colorx colorz 条件2、x、y、z的坐标满足 y − x z − y(即 y 在 x 和 z 的中心位置) …

scipy csr_matrix: understand indptr

See https://stackoverflow.com/questions/52299420/scipy-csr-matrix-understand-indptr

Esp8266 - USB开关分享(开源)

文章目录 简介推广自己gitee项目地址:嘉立创项目地址:联系我们 功能演示视频原理图嘉立创PCB开源地址原理图PCB预览 固件烧录代码编译烧录1. 软件和驱动安装2. 代码编译1. 安装所需要的依赖库文件2. 下载源代码3. 烧录代码 使用说明1. 设备配网2. 打开设备操作页面3…

NAT的知识点和实现

1.NAT的作用: (1)、把内网私网IP转换公网IP; (2)、隐藏内网,起到保护内网作用; (3)、适当的缓解的IPv4地址空间枯竭; (4&#xff…

[RTOS 学习记录] 复杂工程项目的管理

[RTOS 学习记录] 复杂工程项目的管理 这篇文章是我阅读《嵌入式实时操作系统μCOS-II原理及应用》后的读书笔记,记录目的是为了个人后续回顾复习使用。 前置内容: 工程管理工具make及makefile 文章目录 1 批处理文件与makefile的综合使用1.1 批处理文件…

Qt实现XYModem协议(五)

1 概述 XMODEM协议是一种使用拨号调制解调器的个人计算机通信中广泛使用的异步文件运输协议。这种协议以128字节块的形式传输数据,并且每个块都使用一个校验和过程来进行错误检测。使用循环冗余校验的与XMODEM相应的一种协议称为XMODEM-CRC。还有一种是XMODEM-1K&am…

4月23号总结

java实现发送邮件 在做聊天室项目的时候&#xff0c;由于需要发送邮箱验证码&#xff0c;所以自己查找了这方面的内容。 首先需要在Maven里面依赖 <dependency><groupId>com.sun.mail</groupId><artifactId>javax.mail</artifactId><versio…

英伟达AI系列免费公开课

英伟达公开课官网地址 Augment your LLM Using Retrieval Augmented Generation Building RAG Agents with LLMs langchain的workflow: ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/c90cb157c9c84bb5b3da380ec56f5c2a.png Generative AI Explained

Linux系统中安装MySQL

1、在电脑中安装虚拟机 2、df -h查看光盘是否挂载&#xff0c;没挂载用mount -o ro /dev/sr0 /media命令挂载 3、进入etc/yum.repos.d目录查看仓是否配置&#xff0c;若配置进行下一一步&#xff0c;未配置则进行配置 配置软件仓库 [rootlocalhost yum.repos.d]# vim rhle.r…

Linux中文件描述符与重定向的深入探索

目录 1. 理解C语言的文件操作函数 2. 操作系统的文件操作接口 3. 文件描述符详解和其内核本质 4. 如何理解Linux下一切皆文件 5. Linux中的重定向 5.1 输出重定向 5.2 追加重定向 5.3 输入重定向 6. 结合文件描述符理解重定向 7.重定向的系统调用 在Linux操作系统中&a…

springboot整合mybatis-plus模版

1.创建springboot项目 Maven类型Lombok依赖Spring Web 依赖MySQL Driver依赖pom.xml&#xff1a;<?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/…

上门废品回收小程序,互联网回收拥有哪些特点?

随着社会的进步&#xff0c;人们的生活水平不断提高&#xff0c;产生的可回收物也在不断上升&#xff0c;每年垃圾站都能产生大量的可回收物&#xff0c;这也造成了资源的浪费。 目前&#xff0c;加快发展回收模式&#xff0c;提高我国回收效率成为了当下回收市场发展的重要方…

[笔试强训day04]

文章目录 WY22 Fibonacci数列NC242 单词搜索BC140 杨辉三角 WY22 Fibonacci数列 WY22 Fibonacci数列 #include<iostream> #include<cmath>using namespace std;int n;int main() {cin>>n;int a0,b1,c1;while(n>c){ab;bc;cab;}int ansmin(n-b,c-n);cout&l…

windows mysql8 安装后 提示密码不对,修改下密码认证方式就可以了

Windows上安装MySQL8后提示密码不对的问题可以通过以下步骤解决&#xff1a; 安装MySQL8 首先&#xff0c;你需要下载并安装MySQL8。你可以从MySQL官方网站下载符合你操作系统版本的安装包。 安装地址是&#xff1a;MySQL :: Download MySQL Installer 安装过程中&#xff…

ACRN Intel推出的虚拟机是啥样的?

前言 ACRN作为Intel为工控领域推出的一个小型化的虚拟机&#xff0c;它的特点主要有这么几个&#xff1a; 1.针对Intel的芯片做了非常强的优化 2.RT-VM实时虚拟机的实时性很好 3.CACHE缓存技术发挥的好 4.TCC技术 / 当然不是所有intel的芯片都支持&#xff0c;&#xff0c…

鸿蒙(HarmonyOS)性能优化实战-多线程共享内存

概述 在应用开发中&#xff0c;为了避免主线程阻塞&#xff0c;提高应用性能&#xff0c;需要将一些耗时操作放在子线程中执行。此时&#xff0c;子线程就需要访问主线程中的数据。ArkTS采用了基于消息通信的Actor并发模型&#xff0c;具有内存隔离的特性&#xff0c;所以跨线…