---
title: "20240409-Rust-03-数据类型"
source: https://www.iowill.com/blog/2024/09/rust-03-data-types/
date: 2024-09-20
updated: 2026-10-14
tags: [Rust, 数据类型, 语言基础]
categories: [编程开发]
---

# 20240409-Rust-03-数据类型

> Rust 的每个值都有确切的**数据类型**（*data type*），该类型告诉 Rust 数据是被指定成哪类数据，从而让 Rust 知道如何使用该数据。在本节中，我们将介绍两种数据类型：标量类型和复合类型。
> 
> 请记住 Rust 是一种**静态类型**（*statically typed*）的语言，这意味着它必须在编译期知道所有变量的类型。

当类型可能是多种情况或编译器无法推导和确定时，必须加上一个类型标注：

```rust
let guess: u32 = "42".parse().expect("Not a number!");
```

## 标量类型

> **标量**（*scalar*）类型表示单个值。Rust 有 4 个基本的标量类型：整型、浮点型、布尔型和字符。你可能从其他语言了解过这些类型。下面我们深入了解它们在 Rust 中的用法。

### 整数类型

> **整数**（*integer*）是没有小数部分的数字。我们在第 2 章使用过一个整数类型（整型），即 `u32` 类型。此类型声明表明它关联的值应该是占用 32 位空间的无符号整型（有符号整型以 `i` 开始，`i` 是英文单词 *integer* 的首字母，与之相反的是 `u`，代表无符号 `unsigned` 类型）。表 3-1 显示了 Rust 中的内置的整数类型。我们可以使用这些定义形式中的任何一个来声明整数值的类型。

表 3-1: Rust 中的整型

| 长度 | 有符号类型 | 无符号类型 |
| --- | --- | --- |
| 8 位 | `i8` | `u8` |
| 16 位 | `i16` | `u16` |
| 32 位 | `i32` | `u32` |
| 64 位 | `i64` | `u64` |
| 128 位 | `i128` | `u128` |
| arch | `isize` | `usize` |

> 每个定义形式要么是有符号类型要么是无符号类型，且带有一个显式的大小。
> 
> 此外，`isize` 和 `usize` 类型取决于程序运行的计算机体系结构，在表中表示为“arch”：若使用 64 位架构系统则为 64 位，若使用 32 位架构系统则为 32 位。
> 
> 你可按表 3-2 中所示的任意形式来编写整型的字面量。注意，可能属于多种数字类型的数字字面量允许使用类型后缀来指定类型，例如 `57u8`。数字字面量还可以使用 `_` 作为可视分隔符以方便读数，如 `1_000`，此值和 `1000` 相同。

表 3-2: Rust 的整型字面量

| 数字字面量 | 示例 |
| --- | --- |
| 十进制 | `98_222` |
| 十六进制 | `0xff` |
| 八进制 | `0o77` |
| 二进制 | `0b1111_0000` |
| 字节 (仅限于 `u8`) | `b'A'` |

> #### [整型溢出](https://www.rustwiki.org.cn/zh-CN/book/ch03-02-data-types.html#%E6%95%B4%E5%9E%8B%E6%BA%A2%E5%87%BA)
> 
> 比方说有一个 `u8` ，它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值，比如 256，则会发生**整型溢出**（*integer overflow*），这会导致两种行为的其中一种。当在调试（debug）模式编译时，Rust 会检查整型溢出，若存在这些问题则使程序在编译时 *panic*。Rust 使用 panic 这个术语来表明程序因错误而退出。第 9 章 [“`panic!` 与不可恢复的错误”](https://www.rustwiki.org.cn/zh-CN/book/ch09-01-unrecoverable-errors-with-panic.html)会详细介绍 panic。
> 
> 在当使用 `--release` 参数进行发布（release）模式构建时，Rust **不**检测会导致 panic 的整型溢出。相反当检测到整型溢出时，Rust 会进行一种被称为二进制补码包裹（*two’s complement wrapping*）的操作。简而言之，大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下，256 变成 0，257 变成 1，依此类推。程序不会 panic，但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。
> 
> 要显式处理溢出的可能性，可以使用标准库针对原始数字类型提供的以下一系列方法：
> 
> -   使用 `wrapping_*` 方法在所有模式下进行包裹，例如 `wrapping_add`
> -   如果使用 `checked_*` 方法时发生溢出，则返回 `None` 值
> -   使用 `overflowing_*` 方法返回该值和一个指示是否存在溢出的布尔值
> -   使用 `saturating_*` 方法使值达到最小值或最大值

### 浮点类型

> **浮点数**（*floating-point number*）是带有小数点的数字，在 Rust 中浮点类型（简称浮点型）数字也有两种基本类型。Rust 的浮点型是 `f32` 和 `f64`，它们的大小分别为 32 位和 64 位。默认浮点类型是 `f64`，因为在现代的 CPU 中它的速度与 `f32` 的几乎相同，但精度更高。所有浮点型都是有符号的。
> 
> 浮点数按照 IEEE-754 标准表示。`f32` 类型是单精度浮点型，`f64` 为双精度浮点型。

下面是一个演示浮点数的示例：

```rust
fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}
```

### 数字运算

> Rust 的所有数字类型都支持基本数学运算：加法、减法、乘法、除法和取模运算。整数除法会向下取整。

下面代码演示了各使用一条 `let` 语句来说明相应数字运算的用法：

```rust
fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // remainder
    let remainder = 43 % 5;
}
```

\==经过尝试，和C++等语言不同的是，Rust的运算表达式中所有参与运算的数的类型必须是相同的，不同类型不能放在一起运算，如以下几种写法都是会报错的==

```rust
let x = 12i32 + 13u32;
let x = 12.2 + 11;
let x = 12i32 + 13i64;
```

### 布尔类型

> 和大多数编程语言一样，Rust 中的布尔类型也有两个可能的值：`true` 和 `false`。布尔值的大小为 1 个字节。Rust 中的布尔类型使用 `bool` 声明。

例如：

```rust
fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}
```

### 字符类型

> Rust 的 `char`（字符）类型是该语言最基本的字母类型

例子：

```rust
fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}
```

> 注意，我们声明的 `char` 字面量采用单引号括起来，这与字符串字面量不同，字符串字面量是用双引号括起来。==Rust 的字符类型大小为 **4 个字节**\==，表示的是一个 Unicode 标量值，这意味着它可以表示的远远不止是 ASCII。标音字母，中文/日文/韩文的文字，emoji，还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。Unicode 值的范围为 `U+0000` ~ `U+D7FF` 和 `U+E000`~\`U+10FFFF\`。不过“字符”并不是 Unicode 中的一个概念，所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。

## 复合类型

> **复合类型**（*compound type*）可以将多个值组合成一个类型。Rust 有两种基本的复合类型：元组（tuple）和数组（array）。

### 元组类型

> 元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的：声明后，它们就无法增长或缩小。

我们通过在小括号内写入以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型，并且元组中不同值的类型不要求是相同的。我们在下面示例中添加了可选的类型标注：

```rust
fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}
```

变量 `tup` 绑定到整个元组，因为元组被认作是单个复合元素。 想从元组中获取个别值，我们可以使用模式匹配来解构（destructure）元组的一个值，如下所示：

```rust
fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}
```

该程序首先创建一个元组并将其绑定到变量 `tup` 上。 然后它借助 `let` 来使用一个模式匹配 `tup`，并将它分解成三个单独的变量 `x`、`y` 和 `z`。 这过程称为**解构**（*destructuring*），因为它将单个元组分为三部分。最后，程序打印出 `y` 值，为 `6.4`。

除了通过模式匹配进行解构外，我们还可以使用一个句点（`.`）连上要访问的值的索引来直接访问元组元素。例如：

```rust
fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}
```

该程序创建一个元组 `x`，然后通过使用它们的索引为每个元素创建新的变量。和大多数编程语言一样，元组中的第一个索引为 0。

没有任何值的元组 `()` 有一个特殊的名字，**单元**(*unit*)，这个值及其对应的类型都被写成`()`，代表空值或空返回类型。该类型被称为**单元类型**（*unit type*），该值被称为**单元值**（*unit value*）。如果表达式不返回任何其他值，就隐式地返回单元值。

这里原文是：

> The tuple without any values has a special name, *unit*. This value and its corresponding type are both written `()` and represent an empty value or an empty return type. Expressions implicitly return the unit value if they don’t return any other value.

译文是：

> 没有任何值的元组 `()` 是一种特殊的类型，只有一个值，也写成 `()`。该类型被称为**单元类型**（*unit type*），该值被称为**单元值**（*unit value*）。如果表达式不返回任何其他值，就隐式地返回单元值。

“没有任何值的元组 `()` 是一种特殊的类型，只有一个值，也写成 `()`“这句话感觉翻译的不是很好？看了原文才明白。

### 数组类型

> 将多个值组合在一起的另一种方式就是使用**数组**（*array*）。与元组不同，数组的每个元素必须具有相同的类型。与某些其他语言中的数组不同，Rust 中的数组具有固定长度。

我们在方括号内以逗号分隔的列表形式将值写到数组中：

```rust
fn main() {
    let a = [1, 2, 3, 4, 5];
}
```

> 当你希望将数据分配到栈（stack）而不是堆（heap）时，或者当你希望确保始终具有固定数量的元素时，数组特别有用。但它们不像 vector （译注：中文字面翻译为“向量”，在 Rust 中意义为“动态数组，可变数组”）类型那么灵活。vector 类型类似于标准库中提供的集合类型，其大小**允许**增长或缩小。如果不确定是使用数组还是 vector，那就应该使用一个 vector。
> 
> 不过当你明确元素数量不需要改变时，数组会更有用。

使用方括号编写数组的类型，其中包含每个元素的类型、分号，然后是数组中的元素数，如下所示：

```rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
```

对于每个元素都相同的情况，还可以通过指定初始值、后跟分号和方括号中的数组长度来初始化数组，如下所示：

```rust
let a = [3; 5];
```

这种写法与 `let a = [3, 3, 3, 3, 3];` 效果相同，但更简洁。

#### 访问数组元素

数组是可以在栈上分配的已知固定大小的单个内存块。可以使用索引访问数组的元素，如下所示：

```rust
fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}
```

##### 无效的数组元素访问

如果尝试访问超出数组末尾的数组元素，会发生什么？

```rust
use std::io;

fn main() {
    let a = [1, 2, 3, 4, 5];

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!(
        "The value of the element at index {} is: {}",
        index, element
    );
}
```

此代码编译成功。如果使用 `cargo run` 来运行此代码并输入 0、1、2、3 或 4，则程序将打印数组对应索引的值。如果输入的是超出数组末尾的数字，例如 10，则会看到类似以下的输出：

```console
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
```

该程序在索引操作中使用无效值时导致**运行时**（*runtime*）错误。程序退出并显示错误消息，未执行后面的 `println!` 语句。当你尝试使用索引访问元素时，Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度，Rust 会出现 `panic`。这种检查必须在运行时进行，尤其是在这种情况下，因为编译器可能无法知道用户之后运行代码时将输入什么值。

这是 Rust 在实践中安全原则的第一个例子。在很多低级语言中，并不进行这种检查，而且在你使用不正确的索引时，可以访问无效的内存。Rust 通过立即退出来的方式防止这种错误，而不是允许内存访问并继续运行程序。
