在 Rust 中,每一个值都属于某一个 数据类型data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。

Rust 是 静态类型statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时,比如第二章的 “比较猜测的数字和秘密数字” 使用 parse 将 String 转换为数字时,必须增加类型注解,像这样:

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

如果不像上面的代码这样添加类型注解 : u32,Rust 会显示如下错误,这说明编译器需要我们提供更多信息,来了解我们想要的类型:

:::tips $ cargo build

Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)

error[E0282]: type annotations needed

—> src/main.rs:2:9

|

2 | let guess = “42”.parse().expect(“Not a number!”);

| ^^^^^

|

help: consider giving guess an explicit type

|

2 | let guess: _ = “42”.parse().expect(“Not a number!”);

| +++

For more information about this error, try rustc --explain E0282.

error: could not compile no_type_annotations due to previous error

:::

标量类型

标量(scalar)类型代表单独的值。Rust 有四种基本的标量类型:整形、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解他们在 Rust 中是如何工作的。

整形

整数 是一个没有小数部分的数字。

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize
每一个变体都可以是有符号或无符号的,并有一个明确的大小。有符号无符号 代表数字能否为负值,换句话说,这个数字是否有可能是负数(有符号数),或者永远为正而不需要符号(无符号数)。 每一个有符号的变体可以储存包含从 -[2n - 1] 到 2n - 1- 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

另外 isizeusize类型依赖运行程序的计算机架构:64位架构上它们是64位的,32位架构上它们是32位的。

可以使用表格下面表格中的任何一种形式编写数字字面值。请注意可以是多种数字类型的数字字面值允许使用类型后缀,例如 57u8 来指定类型,同时也允许使用 _ 做为分隔符以方便读数,例如1_000,它的值与你指定的 1000 相同。
数字字面值 例子
Decimal (十进制) 98_222
Hex (十六进制) 0xff
Octal (八进制) 0o77
Binary (二进制) 0b1111_0000
Byte (单字节字符)(仅限于u8) b’A’
  • 十进制:没有前缀,直接写数字,如 422023
  • 十六进制:以 0x 开头,如 0xff 表示十六进制的 255。
  • 八进制:以 0o 开头,如 0o77 表示八进制的 63。
  • 二进制:以 0b 开头,如 0b1111 表示二进制的 15。
  • 大小类型后缀:可以添加类型后缀来明确数值的类型,如 57u8 表示一个 u8 类型的 57。
ust 的默认类型通常是个不错的起点,数字类型默认是 i32isizeusize 主要作为某些集合的索引。
整型溢出

:::info

整型溢出
比方说有一个 u8 ,它可以存放从零到 255 的值。那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(“integer overflow” ),这会导致以下两种行为之一的发生。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。第九章 “panic!与不可恢复的错误” 部分会详细介绍 panic。 使用 —release flag 在 release 模式中构建时,Rust 不会检测会导致 panic 的整型溢出。相反发生整型溢出时,Rust 会进行一种被称为二进制补码 wrapping(two’s complement wrapping)的操作。简而言之,比此类型能容纳最大值还大的值会回绕到最小值,值 256 变成 0,值 257 变成 1,依此类推。程序不会 panic,不过变量可能也不会是你所期望的值。依赖整型溢出 wrapping 的行为被认为是一种错误。 为了显式地处理溢出的可能性,可以使用这几类标准库提供的原始数字类型方法:
  • 所有模式下都可以使用 wrapping_* 方法进行 wrapping,如 wrapping_add
  • 如果 checked_* 方法出现溢出,则返回 None
  • overflowing_* 方法返回值和一个布尔值,表示是否出现溢出
  • saturating_* 方法在值的最小值或最大值处进行饱和处理

:::

浮点型
Rust 也有两个原生的 浮点数floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。 这是一个展示浮点数的实例:
  1. fn main() {
  2. let x = 2.0; // f64
  3. let y: f32 = 3.0; // f32
  4. }
浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。
数值运算
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。整数除法会向零舍入到最接近的整数。下面的代码展示了如何在 let 语句中使用它们:
  1. fn main() {
  2. // addition
  3. let sum = 5 + 10;
  4. // subtraction
  5. let difference = 95.5 - 4.3;
  6. // multiplication
  7. let product = 4 * 30;
  8. // division
  9. let quotient = 56.7 / 32.2;
  10. let truncated = -5 / 3; // 结果为 -1
  11. // remainder
  12. let remainder = 43 % 5;
  13. }
布尔类型
正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:truefalse。Rust 中的布尔类型使用 bool 表示。例如:
  1. fn main() {
  2. let t = true;
  3. let f: bool = false; // with explicit type annotation
  4. }
使用布尔值的主要场景是条件表达式,例如 if 表达式。在 “控制流”(“Control Flow”) 部分将介绍 if 表达式在 Rust 中如何工作。
字符类型
Rust 的 char 类型是语言中最原生的字母类型。下面是一些声明 char 值的例子:
  1. fn main() {
  2. let c = 'z';
  3. let z: char = 'ℤ'; // with explicit type annotation
  4. let heart_eyed_cat = '😻';
  5. }
注意,我们用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust 的 char 类型的大小为四个字节 (four bytes),并代表了一个 Unicode 标量值(Unicode Scalar Value),这意味着它可以比 ASCII 表示更多内容。在 Rust 中,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。Unicode 标量值包含从 U+0000 U+D7FF U+E000 U+10FFFF 在内的值。不过,“字符” 并不是一个 Unicode 中的概念,所以人直觉上的 “字符” 可能与 Rust 中的 char 并不符合。第八章的 “使用字符串储存 UTF-8 编码的文本” 中将详细讨论这个主题。
复合类型

复合类型Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

元组类型
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。 我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:
  1. fn main() {
  2. let tup: (i32, f64, u8) = (500, 6.4, 1);
  3. }
tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:
  1. fn main() {
  2. let tup = (500, 6.4, 1);
  3. let (x, y, z) = tup;
  4. println!("The value of y is: {y}");
  5. }
程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,xy z。这叫做 解构destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了 y 的值,也就是 6.4 我们也可以使用点号(.)后跟值的索引来直接访问它们。例如:
  1. fn main() {
  2. let x: (i32, f64, u8) = (500, 6.4, 1);
  3. let five_hundred = x.0;
  4. let six_point_four = x.1;
  5. let one = x.2;
  6. }
这个程序创建了一个元组,x,然后使用其各自的索引访问元组中的每个元素。跟大多数编程语言一样,元组的第一个索引值是 0。 不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。
数组类型
另一个包含多个值的方式是 数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。 我们将数组的值写成在方括号内,用逗号分隔:
  1. fn main() {
  2. let a = [1, 2, 3, 4, 5];
  3. }
当你想要在栈(stack)而不是在堆(heap)上为数据分配空间,或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。

然而,当你确定元素个数不会改变时,数组会更有用。例如,当你在一个程序中使用月份名字时,你更应趋向于使用数组而不是 vector,因为你确定只会有 12 个元素。

  1. let months = ["January", "February", "March", "April", "May", "June", "July",
  2. "August", "September", "October", "November", "December"];
可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
  1. let a: [i32; 5] = [1, 2, 3, 4, 5];

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

你还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:

  1. let a = [3; 5];
变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。
访问数组元素
数组是可以在栈 (stack) 上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:
  1. fn main() {
  2. let a = [1, 2, 3, 4, 5];
  3. let first = a[0];
  4. let second = a[1];
  5. }
在这个例子中,叫做 first 的变量的值是 1,因为它是数组索引 [0] 的值。变量 second 将会是数组索引 [1] 的值 2
无效的数组元素访问
我们访问数组结尾之后的元素会发生什么呢?比如你执行以下代码。
  1. fn main() {
  2. let a = [1, 2, 3, 4, 5];
  3. let last = a[5];
  4. }
此代码编译成功。如果您使用 cargo run 运行此代码并输入 01234,程序将在数组中的索引处打印出相应的值。如果你输入一个超过数组末端的数字,如 5,你会看到这样的输出:

:::info thread ‘main’ panicked at ‘index out of bounds: the len is 5 but the index is 5’, src/main.rs:19:19

note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

:::

程序在索引操作中使用一个无效的值时导致 运行时 错误。程序带着错误信息退出,并且没有执行最后的 println! 语句。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。这种检查必须在运行时进行,特别是在这种情况下,因为编译器不可能知道用户在以后运行代码时将输入什么值。 这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。第九章会更详细地讨论 Rust 的错误处理机制,以及如何编写可读性强而又安全的代码,使程序既不会 panic 也不会导致非法内存访问。