数据类型
在 Rust 中,每一个值都有一个特定 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。
记住,Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时必须增加类型注解。
如果不添加类型注解,Rust会显示错误,提示需要提供更多信息来了解想要的类型。
标量类型
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。你可能在其他语言中见过它们。让我们深入了解它们在 Rust 中是如何工作的。
整型
整型 是一个没有小数部分的数字。
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
架构相关 | isize | usize |
每一个变体都可以是有符号或无符号的,并有一个明确的大小。有符号 和 无符号 代表数字能否为负值,换句话说,这个数字是否有可能是负数(有符号数),或者永远为正而不需要符号(无符号数)。这有点像在纸上书写数字:当需要考虑符号的时候,数字以加号或减号作为前缀;然而,可以安全地假设为正数时,加号前缀通常省略。有符号数以二进制补码形式(two’s complement representation) 存储。
每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。
另外,isize
和 usize
类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。
可以使用表格 3-2 中的任何一种形式编写数字字面值。请注意可以是多种数字类型的数字字面值允许使用类型后缀,例如 57u8 来指定类型,同时也允许使用 _ 做为分隔符以方便读数,例如1_000,它的值与你指定的 1000 相同。
数字字面值 | 例子 |
---|---|
Decimal (十进制) | 98_222 |
Hex (十六进制) | 0xff |
Octal (八进制) | 0o77 |
Binary (二进制) | 0b1111_0000 |
Byte (单字节字符)(仅限于u8) | b'A' |
那么该使用哪种类型的数字呢?如果拿不定主意,Rust 的默认类型通常是个不错的起点,整型默认是 i32
。isize
或 usize
主要作为某些集合的索引。
整形溢出
比方说有一个
u8
,它可以存放从零到255
的值。那么当你将其修改为256
时就会发生 整型溢出(integer overflow ),这会导致以下两种行为之一的发生。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic。panic 这个术语被 Rust 用来表明程序因错误而退出。使用
--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
速度几乎一样,不过精度更高。所有的浮点型都是有符号的。
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数采用 IEEE-754 标准表示。(f32 是单精度浮点数,f64 是双精度浮点数。)
数值运算
Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余。整数除法会向零舍入到最接近的整数。下面的代码展示了如何在 let 语句中使用各种数值运算:
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; // 结果为 -1
// remainder
let remainder = 43 % 5;
}
这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。
布尔类型
正如其他大部分编程语言一样,Rust 中的布尔类型有两个可能的值:true 和 false。Rust 中的布尔类型使用 bool 表示。例如:
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
使用布尔值的主要场景是条件表达式,例如 if 表达式。
字符类型
Rust 的 char
类型是语言中最原始的字母类型。下面是一些声明 char
值的例子:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
注意,我们用单引号声明 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
并不符合。
复合类型
复合类型(Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
元组类型
元组是一个将多个不同类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。
我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
tup
变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:
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
。
我们也可以使用点号(.
)后跟值的索引来直接访问所需的元组元素。例如:
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) 元组。这种值以及对应的类型都写作 ()
,表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。
数组类型
将多个值组合在一起的另一种方式就是使用数组(array)。与元组不同,数组的每个元素必须具有相同的类型。与某些其他语言中的数组不同,Rust 中的数组具有固定长度。
我们在方括号内以逗号分隔的列表形式将值写到数组中:
fn main() {
let a = [1, 2, 3, 4, 5];
}
当你希望将数据分配到栈(stack)而不是堆(heap)时,或者当你希望确保始终具有固定数量的元素时,数组特别有用。但它们不像 vector(译注:中文字面翻译为“向量”,在 Rust 中意义为“动态数组,可变数组”)类型那么灵活。vector 类型类似于标准库中提供的集合类型,其大小允许增长或缩小。如果不确定是使用数组还是 vector,那就应该使用一个 vector。
#![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];
效果相同,但更简洁。
访问数组元素
数组是可以在栈上分配的已知固定大小的单个内存块。可以使用索引访问数组的元素,如下所示:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
在这个例子中,名为 first
的变量将获得值 1,因为它是数组中索引 [0] 处的值。名为 second 的变量将从数组中的索引 [1] 中获取得 2。
无效的数组访问
访问数组中的元素时,必须使用有效的索引。如果使用无效的索引,则会导致运行时错误。
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 通过立即退出来的方式防止这种错误,而不是允许内存访问并继续运行程序。