不可变变量和可变变量及隐藏

不可变变量和可变变量


在Rust中,声明变量需要使用let关键字,其使用格式大致如下:

1
2
3
4
let var_name: type_name;

// 例:声明一个`i32`(32位有符号整型)
let a: i32;

变量声明后必须要赋值(初始化)后才可获取值,可以在声明的同时给变量赋值。

1
2
3
4
5
let a: i32;
// let c = a; // 无法编译,因为a未初始化
a = 3;

let b: i32 = 4;

类型名在大多数情况下可以不用写,编译器会自动根据使用的场景和所赋值推断出变量的类型:

1
2
3
4
5
6
let a = 32; // 此时a是一个`i32`,默认设定整型字面值类型为`i32`

// 下面的代码不用完全理解,只需明白类型推断的实现即可
let b = 1; // 由于b用于索引,此时`1`的类型就会转为`usize`并给`b`赋值
let arr = [3, 2]; // arr是一个数组
let k = arr[b]; // b用于索引,因此b是一个`usize`

注意,变量声后就必须赋值。

与许多语言不同,Rust需要在let和变量名间加上mut(mutable)来使其可变,不添加则不可变。

1
2
3
4
5
6
7
let a;
a = 32;
// a = 4; 此句不可编译,因为a是不可变的(只能赋值一次)

let mut b = 1;
b = 2; // 此句可编译,因为b是可变的

看完上面你可能会一脸懵:变量还分可变和不可变?

回答这个问题前,我先粗浅的解释mut的作用:获取变量的修改权。这意味着:要想在Rust中修改变量,必须先获取修改权修改权即修改变量的权限。因此在声明时加上mut才能修改变量,此时就为可变变量,反之为不可变变量。这么做有几点好处:1. 防止变量被意外改变;2. 方便分析程序。

警告:上述解释非官方解释,修改权也不是官方术语,建议看看官方文挡(|)再下结论。

另外补充说明一下,修改权只能在以下四种情形获取或借用

  1. 获取:声明变量时。

  2. 获取:变量以移动(move)的方式传参给函数时。

  3. 借用:声明可变变量引用(&mut T)时。

  4. 声明可变指针(*mut T)时。

修改权被借用后,直到引用被销毁前,都不能再次从原数据获取引用(因为已经借出去了),只能从借用了修改权的引用上借用修改权。

不可变变量的可变性

有人可能会疑惑,既然不可变,为何不将其当成常量?答:不可变变量在声明时所赋值可以是一个变量的值(例如ler r = 随机数();(Rust没有随机数(),我只是举个例子)),意味着每次程序运行时不可变变量的值都可以不同。因此不可变变量并不能当成常量。

如果你了解过C++的const(readonly)、constexpr,就可以这么认为:let a: i32类似于const int aconst a: i32(const在Rust中则用于声明常量)类似于constexpr int a

常量


<>声明一个常量需要使用const关键字,常量名命名约定是全部单词大写,单词之间加下划线。

Rust中常量会在编译前计算出来(这也是letconst的差别所在),也就是说给常量赋值必须是常量值或常量表达式(能在编译时就能计算出结果),不能是变量,意味着其值在运行时都是确定的,而不像不可变变量一样运行时也可以不确定。

另外,声明常量必须写明常量的类型(既然是常量了,那肯定希望类型也是定下来的,不然运用时有可能会产生歧义或误用)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ✔️,所赋值是字面值常量
const CI: i32 = 12345;
// ✔️,所赋值是字面值常量表达式
const CI_2: i32 = 1 * 2 * 3 * 4;
// ✔️,所赋值是常量表达式
const CI_3: i32 = CI * CI_2 * 2;

let m: i32 = 123;
// ❌,所赋值不是常量
// const CE_2: i32 = m;
// ❌,所赋值不是常量表达式
// const CE_3: i32 = m * 2;

// ❌,声明常量必须写明类型,即使所赋值的类型"感觉"只有一种
// const CE = 12345;

隐藏(shadowing)


注:在通过例子学Rust中,数据被隐藏称作数据被冻结,两者含义相同。

Rust允许声明变量名相同的变量,多个变量之间的类型可不同。使用变量名时,获取的变量为当前作用域上文及父作用域上文最新声明的变量。

上面第二句话可能难以听懂,不妨通过看代码来理解这一句话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn main() {
let x = 1;
{ // 这对花括号一般用于划分作用域,一般用不到,此处为了讲解使用一下
let x = 2;
// print!()格式化打印
// println!()格式化打印并换行
// "x={}"里的{}表示此处将替换成x的值(暂时可以这么认为,后期再展开)
// 比如x为2时输出结果就是x=2
// x为3时输出结果就是x=3
// x为"abc"时输出结果就是x=abc
println!("x={}", x);

let x = 3;
println!("x={}", x);
}
println!("x={}", x);
}

输出结果:

1
2
3
x=2
x=3
x=1

原因不难解释:let声明了一个新变量x,此时获取的变量就不再是原来的x而是新的x,直到离开新变量作用域范围后,才能获取原变量。这种特性就是隐藏(shadowing),因为变量名相同,原变量就被新变量隐藏(shadowing,其实我觉得应该译为遮蔽比较好理解)了,离开新变量的作用域后,原变量就不再被隐藏而可以被获取。

小课堂:有人希望修改一个不可变变量的值,他写了如下代码:

1
2
3
4
5
6
let x = 1; //不可变变量,其所赋值可以是任意变量,这里假设为1
{
let mut x = x; //尝试获取修改权
x += 1;
}
print!("{}", x);

很明显上面的输出结果是1而不是2,很多人能指出错误的原因是新的x隐藏了原有的x,因此修改的是新的x而不是旧的x。但我更想指明一点:let mut获取的修改权是新声明变量的修改权,不是所赋值的修改权。