变量与作用域
变量的声明与初始化
Rust的基本语法格式如下:
fn main(){
let bunnies = 2;
}
语句以分号结尾,用花括号包含语句块。 Rust的语法其实借鉴了很多其他的语言,比如C语言和Python, 所以变量定义的格式看起来也跟很多我们熟悉的其他语言相似。Rust中,使用let
关键字声明一个变量。在上面的例子中, 我们声明了一个变量bunnies
, 并且初始化了它的值为2
.
Rust是一种强类型的语言,那么在上面的语句中,哪里标注了这个变量的类型呢?在Rust编程中,如果Rust能准确的识别这个变量的类型,那么我们不需要显式的标注变量的类型,也不需要像C#那样标注一个auto
表示它的类型是自动识别的。
如果需要显式的标注一个变量的类型,可以像下面的例子一样做, 在变量名后加个:
, 后面再写上变量的类型,如下,i32
代表有符号的32位整型。
fn main(){
let bunnies: i32 = 2;
}
与python类似,rust也可以在一行语句中定义多个变量。如下例子便可以在一行代码中为两个变量初始化:
fn main(){
let (bunnies, carrots) = (8, 50);
}
变量不可变
在Rust中,变量默认其实是不可变的,也就是说,一旦对一个变量赋值以后,其值默认是不可被修改的。这一特点与大多数的其他编程语言都不同,其他编程语言的变量默认是随时可以被重新赋值的。那为什么Rust要将变量设置为默认不可变的呢?这就要提到上一章中我们提到的Rust的三个特性了:
- 内存安全: 如果变量在运行过程中始终不变,这可以避免很多bug的发生,变量不可变这一设计,极大的提高了Rust的内存安全特性。
- 无畏并发: 不变的变量,可以被多个线程在不加锁的情况下共享,这也使得Rust的并发更安全可靠。
- 高性能: 不变的变量,使得编译器可以对其进行额外的优化,从而提高了代码的执行速度,提高了程序运行性能。
但是不得不承认我们在编程中一定会遇到需要修改变量的需求, 如果我们直接修改变量的值,编译便会报错,例如下面的代码:
fn main(){
let bunnies: i32 = 2;
bunnies = 3; // Error!
}
如果运行上面的代码,将会得到下面的报错,可以看到,报错中非常明确的指出了代码的问题所在,并且还指出了修改建议, 在报错的最上面,给出了错误的描述,也就是:不能对不可变变量进行二次赋值。在报错中,也指出了错误所在的位置,第3行第5列。接下来还对整个错误的上下文进行了说明,告诉我们在第2行的时候对变量bunnies
已经赋值,然后再第3行再次对不可变变量bunnies
进行了赋值,因此报错。接着还提出了修改建议,让我们在第2行的变量名前面加上mut
, 使其成为一个可变变量,也许能修复这个问题。在最后一行,如果上面的提示还不能解决问题,还可以运行rustc --explain e0384
来查看错误的完整描述。
按照错误提示,我们将代码修改后如下便可以成功运行了:
fn main(){
let mut bunnies: i32 = 2;
bunnies = 3; // Error!
}
常量
在Rust中,常量(constant)其实也属于变量的一种, 相比普通的不可变变量,它更加的不可变。定义一个常量包含以下四个关键步骤:
- 以
const
而不是let
声明; - 变量名格式为全大写字母加下划线分隔;
- 必须声明变量类型;
- 常量的值必须时编译时可确定值的表达式;
下面是普通变量和常量声明的对比:
let wrap_factor = ask_scotty(); // 变量
const WRAP_FACTOR: f64 = 9.9; // 常量
定义一个常量比变量麻烦很多,那为什么还要用常量呢?
- 常量可以在函数作用域外或者模块外进行定义,而在任意的地方使用;
- 常量会在编译时被静态的写入可执行文件,使得运行速度很快;
- Rust官方在每个发布版本中都对const类型增加了越来越多的功能和优化,在可以使用const的地方,使用const是一个好的选择;
作用域
每个变量都有各自的作用域,只有在变量的作用域中,变量才能被使用。代码的作用域通常是从变量被创建的地方开始,到变量所在的代码块结束, 在这个范围中的子代码块中,变量仍然是可以被访问的。
注: 代码块是一组被花括号包含的语句
fn main() {
let x = 5;
{
let y = 99;
println!("x = {}, y = {}", x, y);
}
println!("x = {}, y = {}", x, y); // Error!
}
在上面的代码中, 变量x
在main
函数的代码块中被定义,其中定义了一个子代码块,在子代码块中定义了一个变量y
, 在子代码块中,x
和 y
都可以被访问, 在子代码块结束时, y
立刻被销毁(Rust中没有任何的垃圾回收器,变量总是在离开作用域后被立即销毁),因此第二个println!
语句不能访问变量y
而发生错误。
然而我们不用担心这会在运行时发生bug, 因为这种错误会在编译时就被暴露出来。
变量隐藏
Rust中,也存在变量隐藏的现象
fn main() {
let x = 5;
{
let x = 99;
println!("x = {}", x);
}
println!("x = {}", x); // Error!
}
运行结果应该如下:
x = 99
x = 5
在上述代码中我们在子代码块外部定义了一个变量x
并赋值为5
, 在子代码块中,x
的值被覆盖,为内层代码块中的值99
。当离开了内层代码块后,内层的变量x
被销毁, x
的值又变回了外层代码块中的5
.
再来看一个例子:
fn main() {
let mut x = 5; // x is mutable
let x = x; // x is now immutable
}
这个例子中,第一个x
被隐藏了,这其实相当于重新声明并初始化了x
这个变量,在编译过程中, Rust甚至能识别到这种情形并优化执行的过程,并不会真的先定义一个可变的x
, 再用一个新的x
去覆盖它,而是直接定义一个不可变的变量x
并为其赋值为5
.
再看一个例子:
fn main() {
let meme = "More cowbell!";
let meme = make_image(meme);
}
在上述代码中, 变量meme
甚至能被改变类型(从字符串变成了图片)。
变量与内存安全
在Rust中,在使用一个变量前,必须确保这个变量被初始化。
情景A
fn main() {
let enigma: i32;
println!("{}", enigma); // Error!
}
可以看到,报错提示我们,变量虽然被声明了,但是没有被初始化。
情景B
fn main() {
let enigma: i32;
if true{
enigma = 42;
}
println!("{}", enigma); // Error!
}
即时是在一个恒为真的判断语句中为变量进行了初始化,编译器仍会报错, 因为判断语句只有在运行时才能被判别最终的结果,因此在编译时没办法确保该变量一定会被初始化。
为了保证变量一定被初始化,可以将上述代码改为如下:
fn main() {
let enigma: i32;
if true{
enigma = 42;
} else {
enigma = 7;
}
println!("{}", enigma); // Error!
}
如果在C语言中使用了一个未初始化的变量会出现什么现象呢,如下代码:
include <stdio.h>
int main(){
int enigma;
printf("%d\n", enigma);
}
这将不会导致编译报错,程序可以正常运行,但是会输出一个不可预测的结果,因为声明变量后, C语言就会在内存分配一个地址,而这个内存地址中存储的是什么数据,我们不得而知,它可能是任何东西。
小结
本章介绍了Rust中变量的种类,声明与赋值方式,以及变量的作用域和隐藏特性。下一章将介绍Rust的函数及模块系统。