数据类型

Rust 中的每个值都有一个特定的数据类型,这告诉 Rust 正在指定什么样的数据,以便它知道如何处理这些数据。我们将研究两种数据类型子集:标量和复合。

请记住,Rust 是一种 静态类型 语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值及其使用方式推断我们想要使用的类型。在可能有多种类型的情况下,例如在第 2 章的 “将猜测与秘密数字进行比较” 部分中,我们将 String 转换为数字类型时使用了 parse,我们必须添加类型注解,如下所示:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

如果我们不添加前面代码中显示的: u32类型注解,Rust 将显示以下错误,这意味着编译器需要我们提供更多信息以确定我们想要使用哪种类型:

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error

您将看到其他数据类型的不同类型注解。

标量类型

一个 标量 类型表示一个单一的值。Rust 有四种主要的标量类型:整数、浮点数、布尔值和字符。你可能从其他编程语言中认识这些类型。让我们来看看它们在 Rust 中是如何工作的。

Integer Types

一个整数是没有小数部分的数字。我们在第2章中使用了一种整数类型,即u32类型。这种类型声明表示其关联的值应该是一个无符号整数(有符号整数类型以i而不是u开头),占用32位空间。表3-1显示了Rust中的内置整数类型。我们可以使用这些变体中的任何一种来声明整数值的类型。

表 3-1:Rust 中的整数类型

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

每个变体可以是带符号的或不带符号的,并且具有明确的大小。 带符号的不带符号的指的是数字是否可以为 负数——换句话说,数字是否需要带有符号(带符号的)或者它将始终为正数,因此可以 不带符号表示(不带符号的)。这就像在纸上写数字:当符号重要时,数字会显示带有加号或减号;然而, 当可以假设数字为正数时,它会显示没有符号。 带符号的数字使用二进制补码表示。

每个有符号变体可以存储从 -(2n - 1) 到 2n - 1 - 1(包括)的数字,其中 n 是该变体使用的位数。所以一个 i8 可以存储从 -(27) 到 27 - 1 的数字,即 -128 到 127。无符号变体可以存储从 0 到 2n - 1 的数字,因此一个 u8 可以存储从 0 到 28 - 1 的数字,即 0 到 255。

此外,isizeusize 类型取决于你的程序运行的计算机的架构,表中表示为“arch”: 如果你在 64 位架构上,则为 64 位;如果你在 32 位架构上,则为 32 位。

您可以使用表 3-2 中显示的任何形式编写整数字面量。请注意,可以是多种数字类型的数字字面量允许使用类型后缀,例如 57u8,来指定类型。数字字面量还可以使用 _ 作为视觉分隔符,使数字更易于阅读,例如 1_000,其值与您指定 1000 时相同。

表 3-2:Rust 中的整数字面量

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

那么你如何知道使用哪种类型的整数呢?如果你不确定,Rust 的默认值通常是很好的起点:整数类型默认为 i32。使用 isizeusize 的主要情况是在对某种集合进行索引时。

Integer Overflow

假设你有一个类型为u8的变量,它可以存储0到255之间的值。如果你尝试将变量更改为该范围之外的值,例如256,将会发生整数溢出,这可能导致两种行为之一。当你在调试模式下编译时,Rust 会包含整数溢出检查,如果发生这种行为,这些检查会导致你的程序在运行时崩溃。Rust 在程序以错误退出时使用崩溃一词;我们将在第9章的“不可恢复的错误与panic!部分中更深入地讨论崩溃。

当你使用 --release 标志在发布模式下编译时,Rust 不会 包含导致 panic 的整数溢出检查。相反,如果发生溢出,Rust 会执行 二进制补码环绕。简而言之,大于类型可容纳的最大值的值会“环绕”到类型可容纳的最小值。对于 u8 类型,值 256 会变为 0,值 257 会变为 1,依此类推。程序不会 panic,但变量将具有一个可能不是你期望的值。依赖整数溢出的环绕行为被视为错误。

为了显式处理溢出的可能性,您可以使用标准库为原始数值类型提供的这些方法族:

  • 在所有模式下使用 wrapping_* 方法进行包装,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None 值。
  • 返回值和一个布尔值,指示是否发生了溢出,使用 overflowing_* 方法。
  • 使用 saturating_* 方法在值的最小值或最大值处饱和。

Floating-Point Types

Rust 也有两种用于浮点数的原始类型,即带有小数点的数字。Rust 的浮点类型是 f32f64,它们的大小分别为 32 位和 64 位。默认类型是 f64,因为在现代 CPU 上,它的速度与 f32 大致相同,但能够提供更高的精度。所有浮点类型都是有符号的。

这里有一个示例,展示了浮点数的实际应用:

文件名: src/main.rs

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

    let y: f32 = 3.0; // f32
}

浮点数根据 IEEE-754 标准表示。f32 类型是单精度浮点数,而 f64 具有双精度。

Numeric Operations

Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取余。整数除法会向零舍入到最近的整数。以下代码展示了如何在 let 语句中使用每个数值运算:

文件名: src/main.rs

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 truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

这些语句中的每个表达式都使用了一个数学运算符,并评估为一个单一的值,然后绑定到一个变量。 附录 B 包含了 Rust 提供的所有运算符的列表。

The Boolean Type

如大多数其他编程语言一样,Rust 中的布尔类型有两种可能的值:truefalse。布尔值占用一个字节。Rust 中的布尔类型使用 bool 指定。例如:

文件名: src/main.rs

fn main() {
    let t = true;

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

使用布尔值的主要方式是通过条件语句,例如 if 表达式。我们将在 “控制流” 部分介绍 Rust 中 if 表达式的工作原理。

The Character Type

Rust 的 char 类型是语言中最基本的字母类型。以下是一些声明 char 值的示例:

文件名: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

请注意,我们使用单引号指定 char 字面量,而字符串字面量则使用双引号。Rust 的 char 类型大小为四个字节,表示一个 Unicode 标量值,这意味着它可以表示比 ASCII 更多的内容。带重音的字母;中文、日文和韩文字符;表情符号;以及零宽度空格都是 Rust 中有效的 char 值。Unicode 标量值的范围从 U+0000U+D7FFU+E000U+10FFFF(包括这两个范围)。然而,“字符”在 Unicode 中并不是一个真正的概念,因此你对“字符”的直觉可能与 Rust 中的 char 不完全匹配。我们将在第 8 章的 “使用字符串存储 UTF-8 编码的文本” 中详细讨论这个主题。

复合类型

复合类型可以将多个值组合成一种类型。Rust 有两种原始复合类型:元组和数组。

The Tuple Type

一个 元组 是将多个具有不同类型的值组合成一种复合类型的一般方法。元组具有固定长度:一旦声明,它们不能增长或缩小。

我们通过在括号内写入以逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,元组中不同值的类型不必相同。我们在本例中添加了可选的类型注解:

文件名: src/main.rs

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

变量 tup 绑定到整个元组,因为元组被视为一个复合元素。要从元组中获取单个值,我们可以使用模式匹配来解构元组值,如下所示:

文件名: src/main.rs

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

    let (x, y, z) = tup;

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

这个程序首先创建一个元组并将其绑定到变量tup。然后 使用带有let的模式来取tup并将其转换为三个独立的 变量,xyz。这被称为解构,因为它将 单个元组分解为三个部分。最后,程序打印y的值,即6.4

我们也可以通过使用点 (.) 后跟我们想要访问的值的索引来直接访问元组元素。例如:

文件名: src/main.rs

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。

不含任何值的元组有一个特殊的名字,单元。这个值及其对应的类型都写作 (),表示一个空值或一个空的返回类型。如果表达式没有返回其他值,它们会隐式地返回单元值。

The Array Type

另一种拥有多个值集合的方法是使用数组。与元组不同,数组中的每个元素必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定的长度。

我们以逗号分隔的列表形式在方括号内写入数组的值:

文件名: src/main.rs

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

数组在你希望数据像我们迄今为止看到的其他类型一样分配在栈上,而不是堆上(我们将在第 4 章)讨论栈和堆)时非常有用,或者当你希望确保始终拥有固定数量的元素时。然而,数组不如向量类型灵活。一个向量是由标准库提供的类似集合类型,可以增长或缩小。如果你不确定是使用数组还是向量,很可能你应该使用向量。第 8 章将更详细地讨论向量。

然而,当您知道元素数量不会改变时,数组更为有用。例如,如果您在程序中使用月份名称,您可能会使用数组而不是向量,因为您知道它将始终包含12个元素:

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

您使用方括号编写数组的类型,其中包含每个元素的类型、一个分号,然后是数组中的元素数量,如下所示:类型[数量]

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

这里,i32 是每个元素的类型。分号后的数字 5 表示数组包含五个元素。

你也可以通过指定初始值,后跟一个分号,然后在方括号中指定数组的长度来初始化数组,使每个元素包含相同的值,如下所示:

#![allow(unused)]
fn main() {
let a = [3; 5];
}

数组 a 将包含 5 个元素,这些元素的初始值都将被设置为 3。这与编写 let a = [3, 3, 3, 3, 3]; 相同,但更为简洁。

Accessing Array Elements

一个数组是一块已知固定大小的内存,可以分配在栈上。你可以使用索引访问数组的元素,如下所示:let a = [1, 2, 3, 4, 5]; let first_element = a[0];

文件名: src/main.rs

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

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

在这个例子中,名为 first 的变量将获得值 1,因为这是数组中索引 [0] 处的值。名为 second 的变量将从数组的索引 [1] 处获得值 2

Invalid Array Element Access

让我们看看如果你尝试访问数组末尾之后的元素会发生什么。假设你运行了这段代码,类似于第2章中的猜数字游戏,从用户那里获取数组索引:

文件名: src/main.rs

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 {index} is: {element}");
}

这段代码编译成功。如果您使用 cargo run 运行此代码并输入 01234,程序将打印出数组中该索引处的对应值。如果您输入一个超过数组末尾的数字,例如 10,您将看到如下输出:

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

程序在使用无效值进行索引操作时导致了运行时错误。程序退出时显示了错误消息,并且没有执行最后的println!语句。当你尝试使用索引访问元素时,Rust 会检查你指定的索引是否小于数组长度。如果索引大于或等于长度,Rust 将会崩溃。这种检查必须在运行时进行,特别是在这种情况下,因为编译器不可能知道用户在稍后运行代码时会输入什么值。

这是 Rust 内存安全原则的一个实例。在许多低级语言中,不会进行这种检查,当你提供一个错误的索引时,可能会访问到无效的内存。Rust 通过立即退出而不是允许内存访问并继续执行来保护你免受此类错误的影响。第 9 章将讨论更多关于 Rust 的错误处理以及如何编写既可读又安全的代码,这些代码既不会引发恐慌也不会允许访问无效内存。