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

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

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 在循环中误为不可变变量赋值

rust 复制代码
// 源代码位置: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 关键字。如下所示。

rust 复制代码
let x;  // 变量声明

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

如下所示。

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

❗️变量初始化避坑指南

变量只能被初始化一次。

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

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

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

❗️变量初始化避坑指南

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

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

rust 复制代码
let mut x = 5;
x = 10; // 赋新值

❗️变量赋值避坑指南

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

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

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

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

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

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

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

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

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

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

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

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

rust 复制代码
// 源代码位置: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>是实现内部可变性的常用智能指针类型。

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

相关推荐
老猿讲编程21 分钟前
一个例子来说明Ada语言的实时性支持
开发语言·ada
Chrikk1 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*1 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue1 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man1 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
萧鼎3 小时前
Python并发编程库:Asyncio的异步编程实战
开发语言·数据库·python·异步
学地理的小胖砸3 小时前
【一些关于Python的信息和帮助】
开发语言·python
疯一样的码农3 小时前
Python 继承、多态、封装、抽象
开发语言·python
^velpro^3 小时前
数据库连接池的创建
java·开发语言·数据库
秋の花3 小时前
【JAVA基础】Java集合基础
java·开发语言·windows