Rust-所有权和移动语义

什么是所有权

拿C语言的代码来打个比方。我们可能会在堆上创建一个对象,然后使用一个指针来管理这个对象:

Foo *p =make_object("args");

接下来,我们可能需要使用这个对象:

use_object(p);

然而,这段代码之后,谁能猜得到,指针p指向的对象究竟发生了什么?它是否被修改过了?它还存在吗,是否已经被释放?是否有另外一个指针现在也同时指向这个对象?我们还能继续读取、修改或者释放这个对象吗?实际上,除了去了解use_object的内部实现之外,我们没办法回答以上问题。

对此,C++进行了一个改进,即通过“智能指针”来描述“所有权”(Ownership)概念。

这在一定程度上减少了内存使用bug,实现了“半自动化”的内存管理。

而Rust在此基础上更进一步,将“所有权”的理念直接融入到了语言之中。

“所有权”代表着以下意义:

  • 每个值在Rust中都有一个变量来管理它,这个变量就是这个值、这块内存的所有者;
  • 每个值在一个时间点上只有一个管理者;
  • 当变量所在的作用域结束的时候,变量以及它代表的值将会被销毁。

拿前面已经讲过的字符串String类型来举例:

在这里插入图片描述
当我们声明一个变量s,并用String类型对它进行初始化的时候,这个变量s就成了这个字符串的“所有者”。如果我们希望修改这个变量,可以使用mut修饰s,然后调用String类型的成员方法来实现。

当main函数结束的时候,s将会被析构,它管理的内存(不论是堆上的,还是栈上的)则会被释放。

我们一般把变量从出生到死亡的整个阶段,叫作一个变量的“生命周期”。

比如这个例子中的局部变量s,它的生命周期就是从let语句开始,到main函数结束。

在上述示例的基础上,若做一点修改:

在这里插入图片描述
这里出现了编译错误。编译器显示,在let s1 =s;语句中,原本由s拥有的字符串已经转移给了s1这个变量。所以,后面继续使用s是不对的。

也就是前面所说的每个值只有一个所有者。变量s的生命周期从声明开始,到move给s1就结束了。

变量s1的生命周期则是从它声明开始,到函数结束。

而字符串本身,由String::from函数创建出来,到函数结束的时候就会销毁。

中间所有权的转换,并不会将这个字符串本身重新销毁再创建。

在任意时刻,这个字符串只有一个所有者,要么是s,要么是s1。

在用变量s初始化s1的时候,并不会造成s的生命周期结束。

这里只会调用string类型的复制构造函数复制出一个新的字符串,于是在后面s1和s都是合法变量。

在Rust中,我们要模拟这一行为,需要手动调用clone()方法来完成:

在这里插入图片描述

在Rust里面,不可以做“赋值运算符重载”,若需要“深复制”,必须手工调用clone方法。

这个clone方法来自于std::clone::Clone这个trait。clone方法里面的行为是可以自定义的。

移动语义

一个变量可以把它拥有的值转移给另外一个变量,称为“所有权转移”。

赋值语句、函数调用、函数返回等,都有可能导致所有权转移。

在这里插入图片描述
所有权转移的步骤分解如下。
在这里插入图片描述

  1. main函数调用create函数。
  2. 在调用create函数的时候创建了字符串,在栈上和堆上都分配有内存。局部变量s是这些内存的所有者。
  3. create函数返回的时候,需要将局部变量s移动到函数外面,这个过程就是简单地按字节复制memcpy。
  4. 同理,在调用consume函数的时候,需要将main函数中的局部变量转移到consume函数,这个过程也是简单地按字节复制memcpy。
  5. 当consume函数结束的时候,它并没有把内部的局部变量再转移出来,这种情况下,consume内部局部变量的生命周期就该结束了。这个局部变量s生命周期结束的时候,会自动释放它所拥有的内存,因此字符串也就被释放了。

Rust中所有权转移的重要特点是,它是所有类型的默认语义。

这里再重复一遍,请大家牢牢记住,Rust中的变量绑定操作,默认是move语义,执行了新的变量绑定后,原来的变量就不能再被使用!一定要记住!

Rust的这一规定非常有利于编译器静态检查。

与之相对的,C++的做法就不一样了,它允许赋值构造函数、赋值运算符重载,因此在出现“构造”或者“赋值”操作的时候,有可能表达的是完全不同的含义,这取决于程序员如何实现重载。C++的这个设计具有巨大的灵活性,但是不恰当的实现也可能造成内存不安全。

而Rust的这一设计大幅降低了语言的复杂度,“移动语义”不可能执行用户的自定义代码,没有任何内存安全风险,而且满足异常安全。

在C++里面,std::vectorv1 =v2;是复制语义,而Rust里面的let v1:Vec=v2 ;是移动语义。如果要在Rust里面实现复制语义,需要显式写出函数调用let v1:Vec=v2.clone();。如果我们在C++中实现移动语义,则需要户自定义实现移动构造函数及移动赋值运算符。

对于“移动语义”,最后还需要强调的一点是,“语义”不代表最终的执行效率。

“语义”只是规定了什么样的代码是编译器可以接受的,以及它执行后的效果可以用怎样的思维模型去理解。

编译器有权在不改变语义的情况下做任何有利于执行效率的优化。

语义和优化是两个阶段的事情。我们可以把移动语义想象成执行了一个memcpy,但真实的汇编代码未必如此。比如:

在这里插入图片描述
完全可能被优化为类似如下的效果:
在这里插入图片描述
编译器可以提前在当前调用栈中把大对象的空间分配好,然后把这个对象的指针传递给子函数,由子函数执行这个变量的初始化。

这样就避免了大对象的复制工作,参数传递只是一个指针而已。

这么做是完全满足移动语义要求的,而且编译器还有权利做更多类似的优化。

复制语义

默认的move语义是Rust的一个重要设计,但是任何时候需要复制都去调用clone函数会显得非常烦琐。

对于一些简单类型,比如整数、bool,让它们在赋值的时候默认采用复制操作会让语言更简单。

比如下面这个程序就可以正常编译通过:

在这里插入图片描述

编译器并没有阻止v1被使用,这是为什么呢?

因为在Rust中有一部分“特殊照顾”的类型,其变量绑定操作是copy语义。

所谓的copy语义,是指在执行变量绑定操作的时候,v2是对v1所属数据的一份复制。

v1所管理的这块内存依然存在,并未失效,而v2是新开辟了一块内存,它的内容是从v1管理的内存中复制而来的。和手动调用clone方法效果一样,let v2 =v1;等效于let v2 =v1.clone();。

使用文件系统来打比方。

copy语义就像“复制、粘贴”操作。操作完成后,原来的数据依然存在,而新的数据是原来数据的复制品。

move语义就像“剪切、粘贴”操作。

操作完成后,原来的数据就不存在了,被移动到了新的地方。

这两个操作本身是一样的,都是简单的内存复制,区别在于复制完以后,原先那个变量的生命周期是否结束。

Rust中,在普通变量绑定、函数传参、模式匹配等场景下,凡是实现了std::marker::Copy trait的类型,都会执行copy语义。

基本类型,比如数字、字符、bool等,都实现了Copy trait,因此具备copy语义。

对于自定义类型,默认是没有实现Copy trait的,但是我们可以手动添上。示例如下:

在这里插入图片描述

编译通过。现在Foo类型也拥有了复制语义。在执行变量绑定、函数参数传递的时候,原来的变量不会失效,而是会新开辟一块内存,将原来的数据复制过来。

绝大部分情况下,实现Copy trait和Clone trait是一个非常机械化的、重复性的工作,clone方法的函数体要对每个成员调用一下clone方法。Rust提供了一个编译器扩展derive attribute,来帮我们写这些代码,其使用方式为#[derive(Copy,Clone)]。

只要一个类型的所有成员都具有Clone trait,我们就可以使用这种方法来让编译器帮我们实现Clone trait了。
在这里插入图片描述

Box类型

Box类型是Rust中一种常用的指针类型。它代表“拥有所有权的指针”,类似于C++里面的unique_ptr(严格来说,unique_ptr更像Option<Box>)。

在这里插入图片描述Box类型永远执行的是move语义,不能是copy语义。

原因大家想想就可以明白,Rust中的copy语义就是浅复制。对于Box这样的类型而言,浅复制必然会造成二次释放问题。

对于Rust里面的所有变量,在使用前一定要合理初始化,否则会出现编译错误。

对于Box/&T /&mut T这样的类型,合理初始化意味着它一定指向了某个具体的对象,不可能是空。

如果用户确实需要“可能为空的”指针,必须使用类型option<Box>。

Rust里面还有一个保留关键字box(注意是小写)。它可以用于把变量“装箱”到堆上。目前这个语法依然是unstable状态,需要打开feature gate才能使用,示例如下:

在这里插入图片描述

Copy的实现条件

并不是所有的类型都可以实现Copy trait。Rust规定,对于自定义类型,只有所有成员都实现了Copy trait,这个类型才有资格实现Copy trait。

常见的数字类型、bool类型、共享借用指针&,都是具有Copy属性的类型。而Box、Vec、可写借用指针&mut等类型都是不具备Copy属性的类型。

对于数组类型,如果它内部的元素类型是Copy,那么这个数组也是Copy类型。对于元组tuple类型,如果它的每一个元素都是Copy类型,那么这个tuple也是Copy类型。

struct和enum类型不会自动实现Copy trait。只有当struct和enum内部的每个元素都是Copy类型时,编译器才允许我们针对此类型实现Copy trait。比如下面这个类型,虽然它的成员是Copy类型,但它本身不是Copy类型:

在这里插入图片描述

自动derive

绝大多数情况下,实现Copy Clone这样的trait都是一个重复而无聊的工作。因此,Rust提供了一个attribute,让我们可以利用编译器自动生成这部分代码。示例如下:
在这里插入图片描述
这里的derive会让编译器帮我们自动生成impl Copy和impl Clone这样的代码。自动生成的clone方法,会依次调用每个成员的clone方法。

通过derive方式自动实现Copy和手工实现Copy有微小的区别。

当类型具有泛型参数的时候,比如struct MyStruct{},通过derive自动生成的代码会自动添加一个T:Copy的约束。

目前,只有一部分固定的特殊trait可以通过derive来自动实现。

将来Rust会允许自定义的derive行为,让我们自己的trait也可以通过derive的方式自动实现。

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

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

相关文章

继承、修饰符、工具类、jar包

目录 1.继承 2.修饰符 3.工具类 4.jar包的制作与使用 1.继承 是什么 1.面向对象的三大特征之一&#xff08;封装、继承、多态&#xff09; 2.可以使得子类具有父类的属性和方法&#xff0c;还可以在子类中重新定义&#xff0c;追加属性和方法。 继承的格式 public class F…

Camtasia2024最新版本如何进行电脑录制屏幕?

在现在的网络互联网时代&#xff0c;越来越多的人走上了自媒体的道路。有些自媒体人会自己在网络上录制精彩视频&#xff0c;也有一些人会将精彩、热门的电影剪辑出来再加上自己给它的配音&#xff0c;做成大家喜欢看的电影剪辑片段。相信不管大家是自己平时有独特的爱好也好、…

瑞吉外卖笔记系列(1) —— 环境配置,后台登录和退出的功能实现

本文档主要介绍软件开发整体流程和瑞吉外卖项目&#xff0c;开发环境搭建步骤&#xff0c;以及简单的后台系统功能实现 文章目录 一、软件开发整体介绍1.1软件开发流程1.2 角色分工1.3 软件环境 二、瑞吉外卖项目介绍2.1 项目介绍2.2 产品原型展示2.3 技术选型2.4 功能架构2.5 …

目标检测应用场景—数据集【NO.25】牛行为检测数据集

写在前面&#xff1a;数据集对应应用场景&#xff0c;不同的应用场景有不同的检测难点以及对应改进方法&#xff0c;本系列整理汇总领域内的数据集&#xff0c;方便大家下载数据集&#xff0c;若无法下载可关注后私信领取。关注免费领取整理好的数据集资料&#xff01;今天分享…

Stream流递归查询部门树

Java 递归查询树是很常见的功能&#xff0c;也有很多写法&#xff0c;小编这里记录stream流递归部门树写法&#xff0c;自从小编用上stream流之后&#xff0c;是爱不释手&#xff0c;的确是个不错的好东西&#xff0c;话不多说&#xff0c;直接上代码 第一步&#xff1a;先创建…

docker下载时报错 /usr/local/bin/docker-compose: 1: cannot open html: No such file

docker 下载时报错 /usr/local/bin/docker-compose: 1: cannot open html: No such file /usr/local/bin/docker-compose: 2: Syntax error: redirection unexpected&#xff0c; 在网上查找了一些解决方法都不对&#xff0c;最后&#xff0c;通过删除/usr/local/bin/docker-co…

arrow,一个神奇的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个神奇的 Python 库 - arrow。 Github地址&#xff1a;https://github.com/arrow-py/arrow 日期和时间处理是许多应用程序中的常见任务&#xff0c;但在 Python 中&#xf…

【备战蓝桥杯】——Day1

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-xKn7nmq36s9pgUXR {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

容器化postgres备份策略

文章目录 1. 策略和背景1.1 背景1.2 备份策略 2. docker-compose的修改2.1 挂载备份目录2.2 备份脚本3.3 重启容器 3. 定时任务 1. 策略和背景 1.1 背景 使用docker-compose管理的postgres数据库需要备份工作目录在 /data/postgres下 1.2 备份策略 要备份的库 shu_han 库 每…

外贸群发邮件最好的软件?群发软件哪个好?

外贸开发信群发软件推荐&#xff1f;做外贸用什么邮件群发软件&#xff1f; 在外贸业务中&#xff0c;与潜在客户建立联系并保持沟通是至关重要的。那么&#xff0c;如何快速有效地发送邮件给大量的潜在客户呢&#xff1f;这就涉及到了外贸群发邮件。蜂邮EDM来探讨一下&#x…

分类预测 | Matlab实现ZOA-CNN-LSTM-Attention斑马优化卷积长短期记忆神经网络注意力机制的数据分类预测【24年新算法】

分类预测 | Matlab实现ZOA-CNN-LSTM-Attention斑马优化卷积长短期记忆神经网络注意力机制的数据分类预测【24年新算法】 目录 分类预测 | Matlab实现ZOA-CNN-LSTM-Attention斑马优化卷积长短期记忆神经网络注意力机制的数据分类预测【24年新算法】分类效果基本描述程序设计参考…

【多线程及高并发 六】并发集合及线程池详解

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是若明天不见&#xff0c;BAT的Java高级开发工程师&#xff0c;CSDN博客专家&#xff0c;后端领域优质创作者 &#x1f4d5;系列专栏&#xff1a;多线程及高并发系列 &#x1f4d5;其他专栏&#xff1a;微服务框架系列、…

打破效率瓶颈:运用Excel提升文秘与行政工作质量

文章目录 一、数据整理二、数据分析三、报表制作四、图表展示五、模板应用六、宏编程七、安全与隐私《Excel高效办公&#xff1a;文秘与行政办公&#xff08;AI版&#xff09;》编辑推荐内容简介作者简介目录获取方式 在现代企业中&#xff0c;文秘与行政办公人员的工作内容繁杂…

neo4j 图数据库 py2neo 操作 示例代码

文章目录 摘要前置NodeMatcher & RelationshipMatcher创建节点查询获取节点节点有则查询&#xff0c;无则创建创建关系查询关系关系有则查询&#xff0c;无则创建 Cypher语句创建节点 摘要 利用py2neo包&#xff0c;实现把excel表里面的数据&#xff0c;插入到neo4j 图数据…

【MYSQL】MYSQL 的学习教程(十二)之 MySQL 啥时候用记录锁,啥时候用间隙锁

在「读未提交」和「读已提交」隔离级别下&#xff0c;都只会使用记录锁&#xff1b;而对于「可重复读」隔离级别来说&#xff0c;会使用记录锁、间隙锁和 Next-Key 锁 那么 MySQL 啥时候会用记录锁&#xff0c;啥时候会用间隙锁&#xff0c;啥时候又会用 Next-Key 锁呢&#xf…

鸿蒙基础开发实战-(ArkTS)像素转换

像素单位转换API的使用 主要功能包括&#xff1a; 展示了不同像素单位的使用。展示了像素单位转换相关API的使用。 像素单位介绍页面 在像素单位介绍页面&#xff0c;介绍了系统像素单位的概念&#xff0c;并在页面中为Text组件的宽度属性设置不同的像素单位&#xff0c;fp…

Linux(Centos7)安装 jenkins(jdk11+jenkins2.375),并配置JDK,Maven,Git,GitLab

安装步骤 1. JDK11安装2. Maven安装3. git安装4. Jenkins2.375安装4.1 设置中文显示4.2 端口,用户权限修改4.3 插件下载4.4 全局工具配置4.4.1 Maven配置4.4.2 JDK配置4.4.3 Git配置 4.5 系统配置4.5.1 Gitee配置 4.6 构建测试 1. JDK11安装 #下载 yum -y install fontconfig …

进阶Docker2:数据卷和挂载目录

目录 准备 删除容器 创建并运行一个容器 数据卷&#xff08;Volumes&#xff09; 挂载数据卷 虚拟机端口映射 挂载目录&#xff08;Bind mounts&#xff09; 挂载目录 挂载文件 部署在线项目 docker 在容器中管理数据主要有两种方式&#xff1a; - 数据卷&#xff0…

图像处理-像素位置的一阶导数和二阶导数

图像处理-像素位置的一阶导数和二阶导数 空间卷积是一种图像处理中常用的技术&#xff0c;用于计算图像中每个像素位置的一阶导数和二阶导数。在这里将解释如何使用卷积操作来实现这些导数的计算。 一阶导数和二阶导数的性质&#xff1a; 一阶导数通常产生粗边缘&#xff1b…

4.2V锂电线性1.2A充电芯片WT4056

4.2V锂电线性1.2A充电芯片WT4056 WT4056是一款专为单节锂离子电池设计的恒流/恒压线性充电器。其简洁的外部电路设计使其非常适用于便携设备的供电&#xff0c;同时兼容USB电源和适配器电源。该充电器内部采用了防倒充电路&#xff0c;无需额外添加外部隔离二极管。通过热反馈…