跳转至

Rust 的变量与类型

变量声明

Rust 语言中通过 let 关键字来声明变量。Rust 是一门静态强类型语言,因此任何一个变量都有一个确定、不可变的类型。如果在声明同时初始化,则可以依靠编译器的类型推断来得到变量类型,不一定需要显式指定类型。

1
2
3
4
let a: i32 = 1; // 完整的变量声明和初始化
let b: i32; // 显式声明类型,未初始化
let c = 1; // 声明同时初始化,类型由编译器推断为 i32
// let d; // 既没有初始化又没有标注类型,编译器无从得知类型,编译无法通过

与 C++ 的对照

Rust 的 let 关键字与 C++11 中引入的 auto 关键字有一些类似之处,例如在声明时初始化的变量可以自行推断类型。

可变性与不可变性

Rust 语言与许多其他语言不同的一点在于变量的默认不可变性,即变量的值默认是不可修改的。

1
2
let a = 1;
// a = 2; // 变量 a 默认不可变,编译无法通过

要声明一个可变的变量,需要加上 mut 关键字。

1
2
let mut a = 1;
a = 2; // 变量 a 可变,OK 

与 C++ 的对照

简单地看,let 声明的变量更类似于 C++ 中用 const 声明的常量,而 let mut 声明的变量更类似于“普通”的 C++ 变量。

这种说法是不严格的,但是这样思考可以快速上手。

Rust 的这种设计看似奇怪,其实也有其用意。一般大部分变量的值实际上都不需要修改,默认不可变可以强制程序员思考变量是否需要修改,从而避免了一些潜在的错误。而在 C++ 中想达到相同的效果,则需要主动使用大量 const 关键字,其繁琐会让大部分程序员不愿意这么做。

如何打印一个变量

1
2
let a: i32 = 1;
println!("{}", a);

这里的 println! 是一个宏,但使用起来与函数很相似。

常量

Rust 中的常量使用 const 关键字声明,常量的值必须在编译期间确定。常量的类型必须显式指定。

1
2
3
const SECONDS_PER_DAY: i32 = 24 * 60 * 60; // 常量的值必须在编译期间确定. 能够通过编译
// const B = 1; // 常量必须显式指定类型,编译无法通过
const c: i32 = 1; // 能够通过编译,但会被编译器警告,因为常量名的规范是 大写字母+下划线

与 C++ 的对照

Rust 的 const 在定位上和 C++ 的 constexpr 基本是一致的。

C++ 中,一般把 constconstexpr 所定义的量都笼统地称为“常量”。但从 Rust 的视角看,只有编译期常量才是真正的“常量”,而 C++ 中用 const 声明的所谓“常量”在语义上只是一个“不可变量”(readonly,只读),其实反而更接近 变量 而不是 常量。

也许可以这样理解:常量是一个「值」;而变量是一个「对象」、其值可能要在运行时才能确定,而这个变量是否可变则是另一个回事。

例如,定义数组要求数组的长度必须是常量表达式。但 C++ 中 const 声明的所谓“常量”并不一定能满足这点;而 C++ 中的 constexpr 与 Rust 中的 const 则都能满足。

变量遮蔽

其英文为 shadowing,可被译为“遮蔽”、“重影”,是 Rust 的一种特殊语法,允许在同一作用域中声明一个与之前变量同名的新变量,从而遮蔽之前的变量。

这相当于把变量名绑定到了一个新的值上,而不是修改了原来的值。这种特性在一些场景下很有用,可以减少变量的数量、减轻给变量取名字的负担。

1
2
3
4
5
6
7
8
let spaces = "   "; // spaces 是一个字符串
let spaces = spaces.len(); // spaces 变成了一个数字

let mut spaces = "   "; // spaces 是一个字符串
// spaces = spaces.len(); // 编译无法通过,因为 spaces 已经是一个字符串,不能再赋值为数字,类型不匹配

let mut num = 1;
num *= 2; // OK

变量类型

基本类型

整数

Rust 中的整数有许多种,按照长度不同、有无符号进行区分。

Rust的多数整数命名遵循“字母 + 数字”这一结构。其中,以字母 i 开头的类型表示它是有符号整数,而以字母 u 开头的类型表示它是无符号整数,字母后面的数字则表示这一类型的长度,从最短的 8 字节到最长的 128 字节,有 i8, u8, i16, u16, i32, u32, i64, u64, i128, u128 这一系列类型。

编译器将整形字面量默认类型推导为 i32

1
2
let a = 1; // a 的类型为(隐式推导为)i32
let b: u32 = 1; // b 的类型(显式声明为)u32

与 C++ 的对照

C/C++ 数据模型 并没有给整形规定具体长度,只规定了不同整形之间长度的比较关系和最小长度。在常见的 64 位机器上,char, short, int, long long 分别表示 8、16、32 和 64 位带符号整数,分别可以对应 Rust 的 i8, i16, i32, i64,无符号整数类似。

在实际使用中,如果需要确定长度的整数,可以使用 cstdlib 中的 int32_t 等类型,它们在不同平台上有不同的 typedef,对应不同的具体类型。

C/C++ 没有统一的 128 位整数标准。

同时,Rust 还提供了两个特殊的类型 isizeusize,这两个类型的长度由平台决定,在 32 位平台上是 32 位,在 64 位平台上是 64 位。这一设计方便了内存中的寻址,如数组下标就接受 usize 而不是 u32 类型。

浮点数

类似整形,Rust 中的浮点型以字母 f 开头,后面是对应的长度,但只有 f32f64 两种类型。

编译器将浮点字面量默认类型推导为 f64

1
2
let f: f32 = 1.0;
let g = 2.0; // 类型默认推导为 f64

布尔值

Rust 中的布尔型为 bool,有且仅有两个值 truefalse

字符类型

Rust 中一个字符由单引号包裹,表示一个 Unicode 字符而非一个 ASCII 字符或字节

1
2
let ascii_char_z = 'z';
let heart_eyed_cat = '😻';

与 C++ 的对照

Rust 在设计时就考虑到了多语言支持,因此采取了这样的设计。如果希望与 C/C++ 中的 unsigned char 对应,即表示一个字节,应当使用 Rust 中的 u8 类型。

复合类型

元组

元组将几个相同或不同的类型组合为一个复合类型。

1
2
let tuple_1: (i32, f64, bool) = (1, 2.0, false); // 显式声明类型
let tuple_2 = (3, 1.0, true); // 隐式推断类型

元组支持通过模式匹配来解构,也可以通过点 . 来访问其元素。

1
2
3
4
5
6
7
let tuple = (1, 2.0, false);
let (x, y, z) = tuple;
// 上面的 let 语句创建了 i32 类型的变量 x、f64 类型的变量 y 和 bool 类型的变量 z
let xx = tuple.0;
let yy = tuple.1;
let zz = tuple.2;
// xx, yy, zz 分别对应 x, y, z

与 C++ 的对照

C++ 语言标准中并没有元组类型,但标准库中提供了功能和语义都相同的 std::tuple。但总的来说,C++ 的元组使用较为繁琐,不如 Rust 中作为基本类型的元组方便。

数组

Rust的数组将固定数目同类型变量储存在一起。

1
2
3
4
5
6
let arr_1: [i32; 5] = [1, 2, 3, 4, 5];
// [i32; 5] 表示包含5个 i32 的数组
let arr_2 = [1.0, 2.0, 3.0, 4.0];
// arr_2 的类型被隐式推断为 [f64; 4]
let arr_long: [i32; 100] = [0; 100];
// arr_long 的类型为 [i32; 100],而且这 100 个元素都是 0

如果需要一个可变长的数组,那么可以使用 Vec

与 C++ 的对照

Rust 的数组可以直接作为参数传递,这一点与 C/C++ 不同(C++ 中的 std::tuple 与 Rust 的数组更加类似)。这是因为 Rust 的数组是在栈上分配的。与 C/C++ 类似的是,数组的长度都是不可变的,变长的线性容器在两门语言中分别叫做 std::Vecstd::vector

Rust 的下标访问自带越界检查。

下面的代码将会在编译时报错:

1
2
let arr = [1, 2, 3];
let element = arr[3]; // 编译器报错:index out of bounds: the length is 3 but the index is 3

下面的代码能够通过编译,但当输入的下标超出数组长度时,程序会立即退出而不允许访问越界的内存:

1
2
3
4
5
6
7
8
use std::io;

let arr = [1, 2, 3];
let mut index = String::new();
io::stdin().read_line(&mut index).expect("Failed to read line"); // 若输入10...
let index: usize = index.trim().parse().expect("Index must be a number");
let element = arr[index]; // ...程序产生运行时错误并退出
println!("The value of element is: {}", element); // 该行不会被执行

单元类型

Rust 中有一个特殊的单元类型 (Unit Type),它的类型是 (),而且它唯一的值也是 ()。例如,当一个表达式或函数什么也不返回时,它的返回类型和返回值就都是 ()

1
2
3
fn some_func() {
    return 0;
}

尝试编译上面的函数,编译器将报告类型不匹配,期望得到 () 而得到了整数。这表明无返回值的函数实质上返回了 ()

与 C++ 的对照

Rust 的 () 类似 C/C++ 的 void,但与之不完全相同。在开始理解时,可以这样对照;但请在认真探究 () 类型的设计时毫不留情地将有关 C/C++ 中的 void 的印象全部抛弃。

关于 Unit Type

在这里,我们不解释 Unit Type 为何设计成如此,也不深究它在 Rust 的其他地方有哪些用处。如果对 Unit Type 的细节感兴趣,可以参考 Rust 官方文档或者查看 这个 StackOverFlow 问题

评论

作者: Ashitemaru (10.92%), ChrisZhang (62.01%), Ethkuil (27.07%)