# Rust 的变量与类型

LLMS index: [llms.txt](/wikis/llms.txt)

---

# Rust 的变量与类型

## 变量声明

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

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

> [!NOTE]
> **与 C++ 的对照**
>
>
> Rust 的 `let` 关键字与 C++11 中引入的 `auto` 关键字有一些类似之处，例如在声明时初始化的变量可以自行推断类型。
### 可变性与不可变性

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

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

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

```rust
let mut a = 1;
a = 2; // 变量 a 可变，OK 
```

> [!NOTE]
> **与 C++ 的对照**
>
>
> 简单地看，`let` 声明的变量更类似于 C++ 中用 `const` 声明的常量，而 `let mut` 声明的变量更类似于“普通”的 C++ 变量。
>
> 这种说法是不严格的，但是这样思考可以快速上手。
Rust 的这种设计看似奇怪，其实也有其用意。一般大部分变量的值实际上都不需要修改，默认不可变可以强制程序员思考变量是否需要修改，从而避免了一些潜在的错误。而在 C++ 中想达到相同的效果，则需要主动使用大量 `const` 关键字，其繁琐会让大部分程序员不愿意这么做。

> [!NOTE]
> **如何打印一个变量**
>
>
> ```rust
> let a: i32 = 1;
> println!("{}", a);
> ```
>
> 这里的 `println!` 是一个宏，但使用起来与函数很相似。
### 常量

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

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

> [!NOTE]
> **与 C++ 的对照**
>
>
> Rust 的 `const` 在定位上和 C++ 的 `constexpr` 基本是一致的。
>
> C++ 中，一般把 `const` 与 `constexpr` 所定义的量都笼统地称为“常量”。但从 Rust 的视角看，只有编译期常量才是真正的“常量”，而 C++ 中用 `const` 声明的所谓“常量”在语义上只是一个“不可变量”（readonly，只读），其实反而更接近 变量 而不是 常量。
>
> 也许可以这样理解：常量是一个「值」；而变量是一个「对象」、其值可能要在运行时才能确定，而这个变量是否可变则是另一个回事。
>
> 例如，定义数组要求数组的长度必须是常量表达式。但 C++ 中 `const` 声明的所谓“常量”并不一定能满足这点；而 C++ 中的 `constexpr` 与 Rust 中的 `const` 则都能满足。
### 变量遮蔽

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

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

```rust
let spaces = "   "; // spaces 是一个字符串
let spaces = spaces.len(); // spaces 变成了一个数字

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

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

> [!NOTE]
> **变量遮蔽的原理**
>
>
> Rust 中变量遮蔽的本质其实是 Rust 的所有权系统。Rust 的变量更接近于 C++ 的指针，表示对内存的引用。
## 变量类型

### 基本类型

**整数**

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

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

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

```rust
let a = 1; // a 的类型为（隐式推导为）i32
let b: u32 = 1; // b 的类型（显式声明为）u32
```

> [!NOTE]
> **与 C++ 的对照**
>
>
> [C/C++ 数据模型](https://zh.cppreference.com/w/cpp/language/types) 并没有给整形规定具体长度，只规定了不同整形之间长度的比较关系和最小长度。在常见的 64 位机器上，`char, short, int, long long` 分别表示 8、16、32 和 64 位带符号整数，分别可以对应 Rust 的 `i8, i16, i32, i64`，无符号整数类似。
>
> 在实际使用中，如果需要确定长度的整数，可以使用 `cstdlib` 中的 `int32_t` 等类型，它们在不同平台上有不同的 `typedef`，对应不同的具体类型。
>
> C/C++ 没有统一的 128 位整数标准。
同时，Rust 还提供了两个特殊的类型 `isize` 和 `usize`，这两个类型的长度由平台决定，在 32 位平台上是 32 位，在 64 位平台上是 64 位。这一设计方便了内存中的寻址，如数组下标就接受 `usize` 而不是 `u32` 类型。

**浮点数**

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

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

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

**布尔值**

Rust 中的布尔型为 `bool`，有且仅有两个值 `true` 和 `false`。

**字符类型**

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

```rust
let ascii_char_z = 'z';
let heart_eyed_cat = '😻';
```

> [!NOTE]
> **与 C++ 的对照**
>
>
> Rust 在设计时就考虑到了多语言支持，因此采取了这样的设计。如果希望与 C/C++ 中的 `unsigned char` 对应，即表示**一个字节**，应当使用 Rust 中的 `u8` 类型。
### 复合类型

**元组**

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

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

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

```rust
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
```

> [!NOTE]
> **与 C++ 的对照**
>
>
> C++ 语言标准中并没有元组类型，但标准库中提供了功能和语义都相同的 `std::tuple`。但总的来说，C++ 的元组使用较为繁琐，不如 Rust 中作为基本类型的元组方便。
**数组**

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

```rust
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`。

```rust
let arr_vec: Vec<i32> = Vec::new();
// Vec::new() 返回一个新的 Vec<i32> 结构体
```

> [!NOTE]
> **与 C++ 的对照**
>
>
> Rust 的数组可以直接作为参数传递，这一点与 C/C++ 不同（C++ 中的 `std::tuple` 与 Rust 的数组更加类似）。这是因为 Rust 的数组是在栈上分配的。与 C/C++ 类似的是，数组的长度都是不可变的，变长的线性容器在两门语言中分别叫做 `std::Vec` 和 `std::vector`。 
Rust 的下标访问自带越界检查。

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

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

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

```rust
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）**，亦称 **单位元**，它的类型是 `()`，而且它唯一的值也是 `()`。例如，当一个表达式或函数什么也不返回时，它的返回类型和返回值就都是 `()`。

```rust
fn some_func() {
    return 0;
}
```

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

> [!NOTE]
> **与 C++ 的对照**
>
>
> Rust 的 `()` 类似 C/C++ 的 `void`，但与之不完全相同。在开始理解时，可以这样对照；但请在认真探究 `()` 类型的设计时毫不留情地将有关 C/C++ 中的 `void` 的印象全部抛弃。
> [!CAUTION]
> **关于 Unit Type**
>
>
> 在这里，我们不解释 Unit Type 为何设计成如此，也不深究它在 Rust 的其他地方有哪些用处。如果对 Unit Type 的细节感兴趣，可以参考 Rust 官方文档或者查看 [这个 StackOverFlow 问题](https://stackoverflow.com/questions/24842271/what-is-the-purpose-of-the-unit-type-in-rust)。
