使用默认不可变的Rust变量会踩什么坑

news2024/11/20 18:26:28

讲动人的故事,写懂人的代码

Rust的变量真的是名不副实。名字中明明有个“变”字,却默认不可变。还美其名曰“不可变变量”。要想让变量名副其实,还必须费心额外加个mut关键字,并必须称其为“可变变量”,才能与前者区分开。这两个名字越琢磨越有趣。

与名不副实的变量相关的概念还真不少。

  • 声明、初始化和绑定变量的语句
  • 可用于变量赋值的一般表达式与控制流表达式
  • 变量的数据类型
  • 可以接受变量作为参数并能将返回值赋给变量的函数
  • 与变量一样都可以存储值的常量

变量名不副实这一点足以让程序员踩坑,而与变量相关的那些概念也暗藏着不少陷阱。那么,程序员在使用Rust的变量及其相关概念时,最容易在哪些场景中踩坑呢?

在这里插入图片描述

3.1 不可变变量绑定值后再为其赋值

对于Rust语言之外的其他编程语言来说,变量默认是可变的。这一点从变量的名字就显而易见。于是不少有其他语言使用背景的初学者,经常踩误为不可变变量赋值的坑。

3.1.1 在循环中误为不可变变量赋值

在循环中求和,是常见的计算方法。当如果忘记Rust的变量默认不可变,那么就会踩为不可变变量赋值的坑,如代码清单3-1所示。

为节省篇幅,本书大部分代码清单只展示最关键的代码(从不连续的行号能看出来)。完整代码可以在用git下载代码后,在代码清单开头注释中标明的源代码位置中查看。

本书代码下载链接为github.com/wubin28/book_LRBACP。本书所有的代码清单,会注明在这个链接中的文件夹位置,以便读者找到相应的没有行号的代码来运行。

下载代码之前,请先安装git。具体的安装步骤,可以询问你最喜欢用的生成式AI聊天工具。

之后,可以运行git clone命令,然后进入文件夹book_LRBACP即能看到所有代码。

代码清单3-1 在循环中误为不可变变量赋值

// 源代码位置:ch03/immutable_misstep
 3     let sum = 0;
 4     for i in 1..=3 {
 5         // sum += i;  // 取消注释这行以查看编译错误
 7     }

代码清单3-1所对应的完整源代码,展示了如何正确和错误地使用变量来计算1到3的累加和。代码通过三种不同的方式来阐述这个问题,突出了"不可变变量绑定值后再误为其赋值"的主旨。限于篇幅,书中只展示和解释重要的代码片段。对于完整源代码中不明白的语句,读者可以自行用最喜欢的生成式AI来解释。

第3行声明了一个不可变变量sum并初始化为0,也就是将0绑定到不可变变量sum上。这里是"误用不可变变量"问题的开始。

第4-7行使用for循环遍历1到3的范围。

第4行是Rust中的一个for循环语句。for 关键字表明要开始一个循环结构。i是循环变量。在每次迭代中,i 会被赋予范围中的下一个值。in这个关键字用来指定循环将遍历一个范围或集合。1..=3是一个范围表达式,它定义了循环将要遍历的值。.. 是Rust的范围语法。1..3 将创建一个不包含上界的范围,即 1 和 2。1..=3 中的 = 符号表示这是一个包含上界的范围。{这个大括号标志着循环体的开始。循环体中的代码将对范围中的每个值执行一次。所以,第4行完整含义是创建一个循环,其中变量 i 将依次取值 1、2 和 3。对于每个值,执行循环体中的代码。

第5行就踩坑里了。如果将第5行的注释去掉,那么这行代码就是其他主流编译语言通常的做法:用赋值的方法试图修改sum。但由于sum是不可变的,这会导致编译错误。

❗️变量避坑指南

不可变变量一旦绑定,就不能再赋值。

如何修复这个问题?代码清单3-1所对应的完整源代码展示了两种方法。一种是在第3行变量sum前,添加mut关键字,使其成为可变变量,这样把第5行的注释取消,编译就不再报错。另一种方法是使用函数式编程的方法,即只用let sum: i32 = (1..=3).sum();这一句,不仅能完成求和与sum的变量绑定工作,还不必把sum声明为mut。这样既省事,代码可读性也好了不少。

讲了半天变量,到底什么是Rust的变量?

❓什么是Rust的变量

Rust的变量是一个命名了的存储位置,它绑定了一个内存中的值,并遵循Rust的所有权规则和生存期规范。

具体来说,Rust的变量有一个标识符(名称),用于在代码中标识它。变量与一个特定的值相关联。这种关联在Rust中被称为"绑定"。变量代表了内存中存储的数据。每个值在任一时刻只能有一个所有者(即变量)。当变量离开作用域时,它所拥有的值会被自动清理。变量的生存期受到严格控制,确保在使用时始终有效。变量命名使用snake_case风格(即单词全小写,单词之间用下划线分隔)。作用域是变量在代码块中可以访问的范围,通常是从声明点开始到包含它的代码块结束,由大括号 {} 界定。

此外,Rust变量还有以下特征。

  • 默认不可变。除非明确声明为可变。不可变变量一旦被绑定就不能更改其值。
  • 类型安全。每个变量都有一个在编译时确定的类型,即使是通过类型推断确定的。
  • 作用域限制。变量的可见性和生存期通常限于声明它的代码块。
  • 支持遮蔽(详见3.3)。可以在同一作用域内多次声明同名变量,新变量会遮蔽旧变量(即旧变量失效)。

上面提到,代码清单3-1的第3行既有变量sum的声明,又有初始化,还提到了绑定。第5行还有赋值。那么变量的声明、初始化、绑定和赋值之间有什么联系和区别?

❓变量的声明、初始化、绑定与赋值

在Rust中,变量的声明、初始化、绑定与赋值是密切相关的概念,它们有一些细微的区别和特定的含义。

变量声明是在程序中引入一个新的变量名。在Rust中,变量声明通常使用 let 关键字。如下所示。

let x;  // 变量声明

变量初始化是给变量赋予一个初始值的过程。在Rust中,初始化通常在声明的同时完成。初始化标志着变量生存期的开始。变量的生存期,指变量从完成声明和初始化开始,到变量因所有权移动、被显式释放或离开作用域而结束的这段时间。

如下所示。

let x = 5;  // 变量声明并初始化,即创建一个绑定

❗️变量初始化避坑指南

变量只能被初始化一次。

**变量绑定结合了声明和初始化的概念。**在Rust中,变量"绑定"这个术语更为常用。当"绑定一个变量"时,通常指的是声明一个变量并将其与一个值关联起来。如上所示。上面这行代码将变量名 x 绑定到值 5 上。

在很多语言中,变量可以先声明后初始化。在Rust中,虽然可以将变量的声明和初始化分开(适用于变量在声明时无法立即确定其值,或变量的初始值需要通过某些计算或函数调用而得到的场景),但在使用变量之前,必须确保它已被初始化。Rust编译器会跟踪变量是否被初始化,以确保在使用前已经初始化。如下所示。

let x;      // 声明不可变变量x
x = 5;      // 初始化x,貌似为不可变变量赋值,但其实不是
println!("{}", x);  // 使用

❗️变量初始化避坑指南

当变量的声明和初始化分开时,初始化不要求变量是可变的。

**赋值是将一个新值存储到已经声明并初始化的可变变量中的过程。**可以多次进行赋值。赋值操作不会改变变量的类型。赋值可以发生在变量生存期内的任何时候。如下所示。

let mut x = 5;
x = 10; // 赋新值

❗️变量赋值避坑指南

只有可变变量才能被赋值。

在Rust中,绑定不仅仅是声明和初始化。它还涉及所有权(ownership)的概念。当绑定一个值到变量时,该变量成为这个值的唯一所有者。

Rust允许重新绑定同名变量,这被称为"遮蔽"(详见3.3)。

默认情况下,Rust中的绑定是不可变的。要创建可变绑定,需要使用 mut 关键字。如下所示。

let mut y = 5;  // 可变绑定
y = 6;          // 允许用赋值语句修改

Rust在绑定时可以进行类型推断,但也允许显式指定类型。如下所示。

let z = 5;       // 整型类型推断默认为 i32
let w: f64 = 5.0;  // 显式指定类型64位浮点数

在Rust中,绑定有明确的生存期,通常持续到变量离开作用域后结束。

变量绑定和赋值可能会涉及所有权的转移,特别是对于非复制(non-Copy)类型的值。

在这里插入图片描述

3.1.2 误为不可变结构体字段赋值

**结构体是Rust中用于创建自定义数据类型的一种方式。**它允许程序员将多个相关的值组合成一个有意义的组。当需要改结构体内某个字段的值的时候,会踩什么可变性的坑?代码清单3-2就是一个踩坑的例子。

代码清单3-2 误为不可变结构体字段赋值

// 源代码位置:ch03/immutable_field_mishap
 1 struct Point {
 2     x: i32,
 3     y: i32,
 4 }
 5 
 6 fn main() {
 8     let point = Point { x: 0, y: 0 };
10 
11     // point.x = 5;  // 取消注释这行以查看编译错误
42 }

代码清单3-2所对应的完整源代码,演示了三种情况:不可变结构体字段的赋值错误、使用可变结构体正确修改字段,以及使用RefCell实现内部可变性。代码的主旨是展示"误为不可变结构体字段赋值"的问题及其解决方法。

第1-4行定义了一个名为Point的结构体,包含两个i32类型的字段xy

第8行创建一个不可变的Point实例point,初始化xy坐标为0。这是踩坑的起点。

第11行踩坑了。这行被注释掉的代码试图用赋值,修改不可变结构体实例pointx坐标,如果取消注释,将导致编译错误。

如何修复这个问题?代码清单3-2所对应的完整源代码,给出了两种修复方法。

第一种方法是在第8行实例point前面,添加mut关键字,使其变为可变实例。

❗️结构体可变性避坑指南

默认情况下,结构体实例是不可变的。要创建可变的结构体实例,需要在声明结构体变量时使用 mut 关键字。结构体的可变性是整体的,不能只将某个字段标记为可变。

第二种方法是在保持point实例不可变的情况下,将其用智能指针RefCell<T>包裹起来。然后利用RefCell<T>的内部可变性,来改变不可变结构体实例point内部字段的值。

❗️在不可变上下文中改变数据的避坑指南

一个不可变变量所拥有的的数据,并不是完全不能修改。使用内部可变性,是能够实现在不可变上下文中改变数据的。内部可变性是 Rust 中的一种设计模式,它允许程序员在拥有不可变引用、不可变变量或不可变实例时改变数据。这看似违反了 Rust 的借用规则,但实际上并不是这样。内部可变性是在语言的安全保证内提供了一种受控的方式来实现可变性。RefCell<T>Cell<T>Mutex<T>RwLock<T>是实现内部可变性的常用智能指针类型。

如果喜欢这篇文章,别忘了给文章点个“赞”,好鼓励小吾继续写哦~😃

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

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

相关文章

对比学习与图像去雾在24TIP和CVPR经典图像去雾论文中的良好结合,展示出模型良好的泛化能力(本期内容较详细 多)

今天主要内容是图像去雾&#xff0c;对比学习&#xff0c;无监督学习&#xff0c;要分享的三篇论文分别是&#xff1a; 21CVPR Contrastive Learning for Compact Single Image Dehazing 24 TIP UCL-Dehaze: Toward Real-World Image Dehazing via Unsupervised Contrastive Le…

stm32f103调试,程序与定时器同步设置

在调试定时器相关代码时&#xff0c;注意到定时器的中断位总是置1&#xff0c;怀疑代码有问题&#xff0c;经过增大定时器的中断时间&#xff0c;发现定时器与代码调试并不同步&#xff0c;这一点对于调试涉及定时器的代码是非常不利的&#xff0c;这里给出keil调试stm32使定时…

HTB:Vaccine[WriteUP]

目录 连接至HTB服务器并启动靶机 1.Besides SSH and HTTP, what other service is hosted on this box? 2.This service can be configured to allow login with any password for specific username. What is that username? 3.What is the name of the file downloaded…

Kafka和RabbitMQ区别

RabbitMQ的消息延迟是微秒级&#xff0c;Kafka是毫秒级&#xff08;1毫秒1000微秒&#xff09; 延迟消息是指生产者发送消息发送消息后&#xff0c;不能立刻被消费者消费&#xff0c;需要等待指定的时间后才可以被消费。 Kafka的单机呑吐量是十万级&#xff0c;RabbitMQ是万级…

10.4 Linux_并发_线程

概述 线程的共享资源&#xff1a; 可执行的指令、静态数据、文件描述符、当前工作目录、用户ID、用户组ID 线程的私有资源&#xff1a; 线程ID、程序计数器PC和相关寄存器、堆栈、错误号、优先级、执行状态和属性 线程编译&#xff1a; gcc <.c文件> -l pthread -o…

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall

数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall 数据量&#xff1a;3k 想要进一步了解&#xff0c;请联系。 DataBall 助力快速掌握数据集的信息和使用方式&#xff0c;会员享有 百种数据集&a…

加密与安全_TOTP 一次性密码生成算法

文章目录 PreTOTP是什么TOTP 算法工作原理TOTP 生成公式TOTP 与 HOTP 的对比Code生成TOTP验证 TOTP使用场景小结 TOTP 与 HOTP 的主要区别TOTP 与 HOTP应用场景比较TOTP 与 HOTP安全性分析 Pre 加密与安全_HTOP 一次性密码生成算法 https://github.com/samdjstevens/java-tot…

YOLO11改进|卷积篇|引入可变核卷积AKConv

目录 一、AKConv卷积1.1AKConv卷积介绍1.2MLCA核心代码 五、添加MLCA注意力机制5.1STEP15.2STEP25.3STEP35.4STEP4 六、yaml文件与运行6.1yaml文件6.2运行成功截图 一、AKConv卷积 1.1AKConv卷积介绍 AKConv允许卷积参数的数量以线性方式增加或减少&#xff0c;而不是传统的平…

C# 表达式与运算符

本课要点&#xff1a; 1、表达式的基本概念 2、常用的几种运算符 3、运算符的优先级 4、常见问题 一 表达式 表达式是由运算符和操作数组成的。、-、*和/等都是运算符&#xff0c;操作数包括文本、常量、变量和表达式等。 二 算术运算符 2.1 算术运算符的使用 三 常见错误 …

Cocotb 学习记录--V01

1. Windows 下安装cocotb pip install cocotb 其他参考&#xff1a; 1.Welcome to cocotb’s documentation! — cocotb 1.9.1 documentation

【Koa】文件上传

主要使用两个 koa 插件&#xff0c;koa-body 里面自带文件上传功能&#xff0c;还有一个 koa-staitc 用于配置静态资源目录&#xff08;可以通过路径直接访问图片&#xff09;。 router const Router require(koa/router); const {upload} require(../controller/user);con…

Spring之生成Bean

Bean的生命周期&#xff1a;实例化->属性填充->初始化->销毁 核心入口方法&#xff1a;finishBeanFactoryInitialization-->preInstantiateSingletons DefaultListableBeanFactory#preInstantiateSingletons用于实例化非懒加载的bean。 1.preInstantiateSinglet…

【RADARSAT Constellation Mission(RCM)卫星星座简介】

RADARSAT Constellation Mission&#xff08;RCM&#xff09;卫星星座是加拿大太空局&#xff08;CSA&#xff09;的下一代C波段合成孔径雷达&#xff08;SAR&#xff09;卫星星座&#xff0c;以下是对其的详细介绍&#xff1a; 一、基本信息 发射时间&#xff1a;2019年6月…

Golang | Leetcode Golang题解之第452题用最少数量的箭引爆气球

题目&#xff1a; 题解&#xff1a; func findMinArrowShots(points [][]int) int {if len(points) 0 {return 0}sort.Slice(points, func(i, j int) bool { return points[i][1] < points[j][1] })maxRight : points[0][1]ans : 1for _, p : range points {if p[0] > …

秒懂Linux之线程

目录 线程概念 线程理解 地址空间&#xff08;页表&#xff0c;内存&#xff0c;虚拟地址&#xff09; 线程的控制 铺垫 线程创建 ​编辑 线程等待 线程异常 线程终止 代码 线程优点 线程缺点 线程特点 线程概念 线程是进程内部的一个执行分支&#xff0c;线程是C…

小程序-全局数据共享

目录 1.什么是全局数据共享 2. 小程序中的全局数据共享方案 MboX 1. 安装 MobX 相关的包 2. 创建 MobX 的 Store 实例 3. 将 Store 中的成员绑定到页面中 4. 在页面上使用 Store 中的成员 5. 将 Store 中的成员绑定到组件中 6. 在组件中使用 Store 中的成员 1.什么是全…

【LeetCode】每日一题 2024_10_2 准时到达的列车最小时速(二分答案)

前言 每天和你一起刷 LeetCode 每日一题~ 大家国庆节快乐呀~ LeetCode 启动&#xff01; 题目&#xff1a;准时到达的列车最小时速 代码与解题思路 今天这道题是经典的二分答案&#xff0c;结合这道题来讲就是&#xff0c;二分列车的速度 我最擅长的两个算法&#xff1a;一…

cGANs with Projection Discriminator

基于映射鉴别器的CGAN 模型中&#xff0c;判别器&#xff08;Discriminator&#xff09;不是通过将条件信息简单地与特征向量拼接&#xff08;concatenate&#xff09;来使用条件信息&#xff0c;而是采用一种基于投影的方式&#xff0c;这种方式更加尊重条件信息在底层概率模…

进程通信——内存映射

进程通信——内存映射 什么是内存映射 内存映射是一种将文件内容映射到进程地址空间的技术&#xff0c;使得进程可以直接访问文件内容&#xff0c;而不需要通过系统调用进行读写操作。内存映射可以提高文件访问的效率&#xff0c;并且可以实现进程间的通信。 内存映射的原理…

【HarmonyOS】时间处理Dayjs

背景 在项目中经常会使用要时间的格式转换&#xff0c;比如数据库返回一个Date数据&#xff0c;你需要转成2024-10-2的格式&#xff0c;鸿蒙的原生SDK中是没有办法实现的&#xff0c;因此&#xff0c;在这里介绍第三方封装好并且成熟使用的库Dayjs。 安装 切换到Entry文件夹下…