数据类型
Rust 中的每个值都属于特定的数据类型,它告诉 Rust 指定了什么样的数据,以便它知道如何处理这些数据。我们将查看两个数据类型子集:标量和复合。
请记住,Rust 是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据值以及我们如何使用它来推断我们想要使用的类型。在可能有多种类型的情况下,例如当我们在
“将猜测与秘密进行比较”中使用 parse 将 String 转换为数字类型时
Number“ 部分,我们必须添加一个类型注解,如下所示:
#![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 中是如何工作的。
整数类型
整数是没有小数分量的数字。我们在第 2 章中使用了一个整数类型,即 u32
类型。此类型声明指示与它关联的值应为占用 32 位空间的无符号整数(有符号整数类型以 i
而不是 u
开头)。表 3-1 显示了 Rust 中内置的整数类型。我们可以使用这些变体中的任何一个来声明整数值的类型。
表 3-1:Rust 中的整数类型
长度 | 签署 | 无符号 |
---|---|---|
8 位 | i8 | u8 |
16 位 | i16 | u16 |
32 位 | i32 | U32 型 |
64 位 | i64 | U64 型 |
128 位 | i128 | U128 型 |
拱 | isize | usize |
每个变体可以是 signed 或 unsigned,并且具有显式大小。
有符号和无符号是指数字是否可以为负数,换句话说,数字是否需要带有符号 (signed),或者它是否只会是正数,因此可以不带符号 (unsigned) 表示。这就像在纸上写数字:当符号很重要时,数字会用加号或减号显示;但是,当可以安全地假设该数字为正数时,它将不带符号显示。有符号数字使用 2 的补码表示形式进行存储。
每个有符号变体都可以存储从 -(2n - 1) 到 2n - 的数字
1 - 1(包括 1 - 1),其中 n 是 variant 使用的位数。所以一个
i8
可以存储从 -(27) 到 27 - 1 的数字,即 -128 到 127。无符号变体可以存储 0 到 2n - 1 之间的数字,因此 u8
可以存储 0 到 28 - 1 之间的数字,等于 0 到 255。
此外,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' |
那么你怎么知道该使用哪种类型的整数呢?如果你不确定,Rust 的默认值通常是不错的起点:整数类型默认为 i32
。使用 isize
或 usize
的主要情况是在为某种集合编制索引时。
整数溢出
假设您有一个 u8
类型的变量,它可以保存 0 到 255 之间的值。如果您尝试将变量更改为该范围之外的值(如 256),则会发生整数溢出,这可能导致以下两种行为之一。当您在调试模式下编译时,Rust 会检查整数溢出,如果发生此行为,会导致程序在运行时出现 panic。Rust 使用术语 panicking 当程序因错误退出时;我们将在 “Unrecoverable Errors with
恐慌!
第 9 章中的部分。
当你使用 --release
标志在发布模式下编译时,Rust 会
不包括检查导致 panic 的整数溢出。相反,如果发生溢出,Rust 会执行 2 的补码包装。简而言之,大于类型可以容纳的最大值的值将“环绕”到该类型可以容纳的值的最小值。对于 u8
,值 256 变为 0,值 257 变为 1,依此类推。程序不会 panic,但变量的值可能不是你期望的。依赖整数溢出的换行行为被视为错误。
要显式处理溢出的可能性,您可以使用标准库为原始数字类型提供的这些方法系列:
使用wrapping_*
方法(如wrapping_add
)在所有模式中包装。
如果checked_*
方法溢出,则返回None
值。
返回值和一个布尔值,指示overflowing_*
方法是否溢出。
使用saturating_*
在值的最小值或最大值处饱和 方法。
浮点类型
Rust 还有两种浮点数的原始类型,它们是带小数点的数字。Rust 的浮点类型是 f32
和 f64
,它们的大小分别为 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
是双精度浮点数。
数值运算
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 提供的所有运算符的列表。
布尔类型
与大多数其他编程语言一样,Rust 中的 Boolean 类型有两个可能的值:true
和 false
。布尔值的大小为 1 个字节。Rust 中的 Boolean 类型是使用 bool
指定的。例如:
文件名: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
使用布尔值的主要方式是通过条件,例如 if
表达。我们将在 Rust 的“控件
Flow“部分。
字符类型
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
类型大小为 4 个字节,表示一个 Unicode 标量值,这意味着它可以表示的不仅仅是 ASCII。重音字母;中文、日文和韩文字符;表情符号;和零宽度的空格都是 Rust 中的有效 char
值。Unicode 标量值的范围从 U+0000
到 U+D7FF
和 U+E000
到 U+10FFFF
(包括 U+E000 到 U+10FFFF)。然而,“字符”在 Unicode 中并不是一个真正的概念,因此你对“字符”的人类直觉可能与 Rust 中的 char
不匹配。我们将在 “存储 UTF-8 编码文本
字符串”。
化合物类型
复合类型可以将多个值分组为一种类型。Rust 有两种原始复合类型:元组和数组。
元组类型
元组是将具有多种类型的多个值分组为一种复合类型的通用方法。元组具有固定长度:一旦声明,它们的大小就不能增大或缩小。
我们通过在括号内编写逗号分隔的值列表来创建元组。元组中的每个位置都有一个类型,元组中不同值的类型不必相同。在此示例中,我们添加了可选的类型注释:
文件名: 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}"); }
该程序首先创建一个 Tuples 并将其绑定到变量 tup
。然后,它使用带有 let
的模式来获取 tup
并将其转换为三个单独的变量 x
、y
和 z
。这称为解构,因为它会中断
单个元组分为三个部分。最后,程序打印
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; }
该程序创建 Tuples x ,然后使用
各自的索引访问 Tuples 的每个元素。与大多数编程语言一样,元组中的第一个索引是 0。
没有任何值的元组具有特殊名称 unit。此值及其相应的类型都写入 ()
并表示空值或空返回类型。如果表达式不返回任何其他值,则隐式返回 unit 值。
数组类型
拥有多个值的集合的另一种方法是使用数组。与 Tuples 不同,数组的每个元素都必须具有相同的类型。与其他一些语言中的数组不同,Rust 中的数组具有固定的长度。
我们将数组中的值写成方括号内的逗号分隔列表:
文件名: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
当你想在栈上分配数据时,数组很有用,就像我们目前看到的其他类型的一样,而不是堆(我们将在第 4 章中更多地讨论栈和堆),或者当你想确保你总是有固定数量的元素时。但是,数组不如 vector 类型灵活。向量是标准库提供的类似集合类型,允许其大小增大或缩小。如果您不确定是使用数组还是向量,那么很可能应该使用向量。第 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
表示数组包含 5 个元素。
您还可以初始化数组以包含每个元素的相同值,方法是指定初始值,后跟分号,然后用方括号指定数组的长度,如下所示:
#![allow(unused)] fn main() { let a = [3; 5]; }
名为 a
的数组将包含 5
个元素,这些元素都将设置为值
最初为 3
。这与编写 let a = [3, 3, 3, 3, 3]相同;
但以更简洁的方式。
访问 Array 元素
数组是可以在堆栈上分配的已知固定大小的单个内存块。您可以使用索引访问数组的元素,如下所示:
文件名: 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
。
无效的数组元素访问
让我们看看如果你尝试访问数组中超过数组末尾的元素会发生什么。假设你运行这段代码,类似于第 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
运行此代码并输入 0
、1
、2
、3
或 4
,程序将打印出数组中该索引处的相应值。如果您输入的是一个超过数组末尾的数字,例如 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 将 panic。此检查必须在运行时进行,尤其是在这种情况下,因为编译器不可能知道用户稍后运行代码时将输入什么值。
这是 Rust 内存安全原则的一个例子。在许多低级语言中,不会进行这种检查,当您提供不正确的索引时,可以访问无效内存。Rust 通过立即退出而不是允许内存访问并继续来保护您免受此类错误的影响。第 9 章讨论了 Rust 的更多错误处理,以及如何编写可读的、安全的代码,既不会 panic 也不允许无效的内存访问。