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 `&` referencefn 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 的 所有权被转移给外面的调用者


引用的规则

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

李沐60_机器翻译数据集——自学笔记

!pip install d2limport os import torch from d2l import torch as d2l下载和预处理数据集 在这个将英语翻译成法语的机器翻译问题中, 英语是源语言(source language), 法语是目标语言(target language)。…

【活动邀请·成都】成都 UG 生成式 AI 工作坊:AI 原生应用的探索与创新!

文章目录 前言一、活动介绍二、报名预约方式三、活动安排四、活动福利五、讲师介绍5.1、陈琪——《如何安全高效地构建生成式 AI 应用》5.2、刘文溢——《AIGC 的产业变革》5.3、胡荣亮——《生成式 AI 在企业应用与实践》5.4、陈明栋——《激发您的灵感,基于生成式…

Swing用法的简单展示

1.简单的登陆界面示例 import javax.swing.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;public class Main extends JFrame {private JTextField usernameField;private JPasswordField passwordField;public Main() {setTitle("登陆界…

第26天:安全开发-PHP应用模版引用Smarty渲染MVC模型数据联动RCE安全

第二十六天 一、PHP新闻显示-数据库操作读取显示 1.新闻列表 数据库创建新闻存储代码连接数据库读取页面进行自定义显示 二、PHP模版引用-自写模版&Smarty渲染 1.自写模版引用 页面显示样式编排显示数据插入页面引用模版调用触发 2.Smarty模版引用 1.下载&#xff1a…

信创传输软件,如何进行国产化替代?

信创产业,即信息技术应用创新产业,它与“863 计划”“973 计划”“核高基” 一脉相承,是我国 IT 产业发展升级采取的长期计划。网络安全事件频发后,中国要确保 IT 相关设施的全部环节国产化,任何不能保证自主可控的环节…

服务器(AIX、Linux、UNIX)性能监视器工具【nmon】使用介绍

目录 ■nmon简介 1.安装 2.使用简介 3.使用(具体使用的例子【CPU】【内存】) 4.采集数据 5.查看log(根据结果,生成报表) 6.分析结果 ■nmon简介 nmon("Nigels performance Monitor"&…

终于有人说明白了session、cookie和token的区别

一、首先介绍一下名词:Session、cookie、token,如下: 1.Session会话:客户端A访问服务器,服务器存储A的数据value,把key返回给客户端A,客户端A下次带着key(session ID)来…

一文浅谈FRTC8563时钟芯片

FRTC8563是NYFEA徕飞公司推出的一款实时时钟芯片,采用SOP-8封装形式。这种封装形式具有体积小、引脚间距小、便于集成等特点,使得FRTC8563能够方便地应用于各种电子设备中。 FRTC8563芯片基于32.768kHz的晶体振荡器工作,这种频率的晶体振荡器…

JavaSE——程序逻辑控制

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

(ICML-2021)从自然语言监督中学习可迁移的视觉模型

从自然语言监督中学习可迁移的视觉模型 Title:Learning Transferable Visual Models From Natural Language Supervision paper是OpenAI发表在ICML 21的工作 paper链接 Abstract SOTA计算机视觉系统经过训练可以预测一组固定的预定目标类别。这种受限的监督形式限制…

服务器基本故障和排查方法

前言 服务器运维工作中遇到的问题形形色色,无论何种故障,都需要结合具体情况,预防为主的思想,熟悉各种工具和技术手段,养成良好的日志分析习惯,同时建立完善的应急预案和备份恢复策略,才能有效…

工业设备管理平台

在这个数字化、智能化的新时代,工业设备管理平台正成为推动工业转型升级的重要力量。在众多平台中,HiWoo Cloud以其卓越的性能、稳定的服务和创新的理念,赢得了广大用户的青睐。今天,就让我们一起走进HiWoo Cloud的世界&#xff0…

WebSocket的原理、作用、常见注解和生命周期的简单介绍,附带SpringBoot示例

文章目录 WebSocket是什么WebSocket的原理WebSocket的作用全双工和半双工客户端【浏览器】API服务端 【Java】APIWebSocket的生命周期WebSocket的常见注解SpringBoot简单代码示例 WebSocket是什么 WebSocket是一种 通信协议 ,它在 客户端和服务器之间建立了一个双向…

123.Mit6.S081-实验1-Xv6 and Unix utilities

今天我们来进行Mit6.S081实验一的内容。 实验任务 一、启动xv6(难度:Easy) 获取实验室的xv6源代码并切换到util分支。 $ git clone git://g.csail.mit.edu/xv6-labs-2020 Cloning into xv6-labs-2020... ... $ cd xv6-labs-2020 $ git checkout util Branch util …

Go 堆内存分配源码解读

简要介绍 在Go的内存分配中存在几个关键结构,分别是page、mspan、mcache、mcentral、mheap,其中mheap中又包括heapArena,具体这些结构在内存分配中担任什么角色呢? 如下图,可以先看一下整体的结构: mcach…

Linux进程详解二:创建、状态、进程排队

文章目录 进程创建进程状态进程排队 进程创建 pid_t fork(void) 创建一个子进程成功将子进程的pid返回给父进程,0返回给新创建的子进程 fork之后有两个执行分支(父和子),fork之后代码共享 bash -> 父 -> 子 创建一个进…

比特币成长的代价

作者:Jeffrey Tucker,作家和总裁。曾就经济、技术、社会哲学和文化等话题广泛发表演讲。编译:秦晋 2017 年之后参与比特币市场的人遇到了与之前的人不同的操作和理想。如今,没有人会太在意之前的事情,说的是 2010-2016…

【全网首发】Mogdb 5.0.6新特性:CM双网卡生产落地方案

在写这篇文章的时候,刚刚加班结束,顺手写了这篇文章。 前言 某大型全国性行业核心系统数据库需要A、B两个物理隔离的双网卡架构方案,已成为行业标准。而最新发布的MogDB 5.0.6的CM新增支持流复制双网段部署,用于网卡级高可用容灾(…

【Linux开发实用篇】备份与恢复

备份 实体机无法做快照,我们可以使用备份和恢复技术 第一种方式 把需要的文件(或者分区)用TAR打包就好,下次恢复的时候进行解压 第二种方式 使用dump 和 restore 指令: 首先安装这两个指令 yum -y install dump, …

参数传递 的案例

文章目录 12 1 输出一个int类型的数组,要求为: [11,22,33,44,55] package com.zhang.parameter; //有关方法的案例 public class MethodTest3 {public static void main(String[] args) {//输出一个int类型的数组,要求为: [11,…