之所以世界上有很多很多类型的书,是因为世界上有很多很多类型的人,而每个人都在追求不同类型的知识。——Lemony Snicket
Rust 语言就是围绕其类型来设计的。Rust 对高性能代码的支持,源自它能让开发人员选择最适合当前场景的数据表示法,并在简单性和成本之间进行合理的权衡。Rust 的内存和线程安全保障也依赖于其类型系统的健全性,而 Rust 的灵活性则源于其泛型类型和特型(Trait)
基于已明确写出的类型,Rust 的类型推断会帮你推断出剩下的大部分类型。
凌乱写法
fn build_vector() -> Vec<i16> { let mut v: Vec<i16> = Vec::<i16>::new(); v.push(10i16); v.push(20i16); v }
简洁写法
fn build_vector() -> Vec<i16> { let mut v = Vec::new(); v.push(10); v.push(20); v }
这两个定义是完全等效的,无论采用哪种方式,Rust 都会生成相同的机器码。类型推断让 Rust 具备了与动态类型语言相近的易读性,并且仍然能在编译期捕获类型错误。
函数可以是泛型的:单个函数就可以处理许多不同类型的值。
在 Python 和 JavaScript 中,所有函数都天生如此:函数可以对任何具备该函数所要求的属性和方法的值进行操作。(这就是通常称为鸭子类型的特征:如果它叫得像鸭子,走路像鸭子,那它就是鸭子。)但也正是这种灵活性让这些语言很难及早发现类型错误,而测试通常是发现此类错误的唯一途径。Rust 的泛型函数为该语言提供了一定程度的灵活性,而且仍然能在编译期捕获所有的类型错误。
虽然泛型函数更灵活,但其效率仍然与非泛型函数一样高。相较于编写能处理所有整数的泛型函数,为每种整数编写一个专用的 sum
函数并没有性能方面的内在优势。
类型 | 说明 | 值 |
i8 、i16 、i32 、i64 、i128 、u8 、u16 、u32 、u64 、u128 |
给定位宽的有符号整数和无符号整数 | 42 、-5i8 、0x400u16 、0o100i16 、20_922_789_888_000u64 、b'*' (u8 字节字面量) |
isize 、usize |
与机器字(32 位或 64 位)一样大的有符号整数和无符号整数 | 137 、-0b0101_0010isize 、0xffff_fc00usize |
f32 、f64 |
单精度 IEEE 浮点数和双精度 IEEE 浮点数 | 1.61803 、3.14f32 、6.0221e23f64 |
bool |
布尔值 | true 、false |
char |
Unicode 字符,32 位宽(4 字节) | '*' 、'\n' 、' 字 ' 、'\x7f' 、'\u{CA0}' |
(char, u8, i32) |
元组,允许混合类型 | ('%', 0x7f, -1) |
() |
“单元”(空元组) | () |
struct S { x: f32, y: f32 } |
具名字段型结构体 | S { x: 120.0, y: 209.0} |
struct T(i32, char); |
元组型结构体 | T (120, 'X') |
struct E; |
单元型结构体,无字段 | E |
enum Attend { OnTime, Late(u32)} |
枚举,或代数数据类型 | Attend::Late(5) 、Attend::OnTime |
Box<Attend> |
Box :指向堆中值的拥有型指针 |
Box::new(Late(15)) |
&i32 、&mut i32 |
共享引用和可变引用:非拥有型指针,其生命周期不能超出引用目标 | &s.y 、&mut v |
String |
UTF-8 字符串,动态分配大小 | " ラ一メン : ramen".to_string() |
&str |
对 str 的引用:指向 UTF-8 文本的非拥有型指针 |
" そば : soba" 、&s[0..12] |
[f64; 4] 、[u8; 256] |
数组,固定长度,其元素类型都相同 | [1.0, 0.0, 0.0, 1.0] 、[b' '; 256] |
Vec<f64> |
向量,可变长度,其元素类型都相同 | vec![0.367, 2.718, 7.389] |
&[u8] 、*mut [u8] |
对切片(数组或向量某一部分)的引用,包含指针和长度 | &v[10..20] 、&mut a[..] |
Option<&str> |
可选值:或者为 None (无值),或者为 Some(v) (有值,其值为 v ) |
Some("Dr."), None |
Result<u64, Error> |
可能失败的操作结果:或者为成功值 Ok(v) ,或者为错误值 Err(e) |
Ok(4096), Err(Error::last_os_error()) |
&dyn Any 、&mut dyn Read |
特型对象,是对任何实现了一组给定方法的值的引用 | value as &dyn Any 、&mut file as &mut dyn Read |
fn(&str) -> bool |
函数指针 | str::is_empty |
(闭包类型没有显式书写形式) | 闭包 | ... |
3.1 固定宽度的数值类型
Rust 类型系统的根基是一组固定宽度的数值类型,选用这些类型是为了匹配几乎所有现代处理器都已直接在硬件中实现的类型。
固定宽度的数值类型可能会溢出或丢失精度,但它们足以满足大多数应用程序的需求,并且要比任意精度整数和精确有理数等表示法快数千倍。如果需要后面提到的那些类型的数值的表示法,可以到 num
crate 中找到它们。
Rust 中数值类型的名称都遵循着一种统一的模式,也就是以“位”数表明它们的宽度,以前缀表明它们的用法,如表 3-2 所示。
表 3-2:Rust 数值类型
大小(位) | 无符号整数 | 有符号整数 | 浮点数 |
8 |
u8 |
i8 |
|
16 |
u16 |
i16 |
|
32 |
u32 |
i32 |
f32 |
64 |
u64 |
i64 |
f64 |
128 |
u128 |
i128 |
|
机器字 | usize |
isize |
在这里,机器字是一个值,其大小等于运行此代码的机器上“地址”的大小,可能是 32 位,也可能是 64 位。
3.1.1 整型
Rust 的无符号整型会使用它们的完整范围来表示正值和 0,如表 3-3 所示。
表 3-3:Rust 无符号整型
类型 | 范围 |
u8 |
0 到 28-1(0 到 255) |
u16 |
0 到 216-1(0 到 65 535) |
u32 |
0 到 232-1(0 到 4 294 967 295) |
u64 |
0 到 264-1(0 到 18 446 744 073 709 551 615 或 1844 京) |
u128 |
0 到 2128-1(0 到大约 3.4×1038) |
usize |
0 到 232-1 或 264-1 |
Rust 的有符号整型会使用二进制补码表示,使用与相应的无符号类型相同的位模式来覆盖正值和负值的范围,如表 3-4 所示。
表 3-4:Rust 有符号整型
类型 | 范围 |
i8 |
-27 到 27-1(-128 到 127) |
i16 |
-215 到 215-1(-32 768 到 32 767) |
i32 |
-231 到 231-1(-2 147 483 648 到 2 147 483 647) |
i64 |
-263 到 263-1(-9 223 372 036 854 775 808 到 9 223 372 036 854 775 807) |
i128 |
-2127 到 2127-1(大约-1.7×1038 到 +1.7×1038) |
isize |
-231 到 231-1 或-263 到 263-1 |
Rust 会使用 u8
类型作为字节值。例如,从二进制文件或套接字中读取数据时会产生一个 u8
值构成的流。
与 C 和 C++ 不同,Rust 会把字符视为与数值截然不同的类型:char
既不是 u8
,也不是 u32
(尽管它确实有 32 位长)。稍后 3.3 节会详细讲解 Rust 的 char
类型。
usize
类型和 isize
类型类似于 C 和 C++ 中的 size_t
和 ptrdiff_t
。它们的精度与目标机器上地址空间的大小保持一致,即在 32 位架构上是 32 位长,在 64 位架构上则是 64 位长。Rust 要求数组索引是 usize
值。用来表示数组或向量大小或某些数据结构中元素数量的值通常也是 usize
类型。
Rust 中的整型字面量可以带上一个后缀来指示它们的类型:42u8
是 u8
类型,1729isize
是 isize
类型。如果整型字面量没有带类型后缀,那么 Rust 就会延迟确定其类型,直到找出一处足以认定其类型的使用代码,比如存储在特定类型的变量中、传给期待特定类型的函数、与具有特定类型的另一个值进行比较,等等。最后,如果有多种候选类型,那么 Rust 就会默认使用 i32
(如果是候选类型之一的话)。如果无法认定类型,那么 Rust 就会将此歧义报告为错误。
前缀 0x
、0o
和 0b
分别表示十六进制字面量、八进制字面量和二进制字面量。
为了让长数值更易读,可以在数字之间任意插入下划线。例如,可以将 u32
的最大值写为 4_294_967_295
。下划线的具体位置无关紧要,因此也可以将十六进制数或二进制数按 4 位数字而非 3 位数字进行分组(如 0xffff_ffff
),或分隔开数字的类型后缀(如 127_u8
)。表 3-5 中展示了整型字面量的一些示例。
表 3-5:整型字面量示例
字面量 | 类型 | 十进制值 |
116i8 |
i8 |
116 |
0xcafeu32 |
u32 |
51966 |
0b0010_1010 |
推断 | 42 |
0o106 |
推断 | 70 |
尽管数值类型和 char
类型是不同的,但 Rust 确实为 u8
值提供了字节字面量。与字符字面量类似,b'X'
表示以字符 X
的 ASCII 码作为 u8
值。例如,由于 A
的 ASCII 码是 65,因此字面量 b'A'
和 65u8
完全等效。只有 ASCII 字符才能出现在字节字面量中。
有几个字符不能简单地放在单引号后面,因为那样在语法上会有歧义或难以阅读。表 3-6 中的字符只能以反斜杠开头的替代符号来书写。
表 3-6:需要替代符号的字符
字符 | 字节字面量 | 等效的数值 |
单引号(' ) |
b''' |
39u8 |
反斜杠(``) | b'\' |
92u8 |
换行(lf ) |
b'\n' |
10u8 |
回车(cr ) |
b'\r' |
13u8 |
制表符(tab ) |
b'\t' |
9u8 |
对于难于书写或阅读的字符,可以将其编码改为十六进制。这种字节字面量形如 b'\xHH'
,其中 HH
是任意两位十六进制数,表示值为 HH
的字节。例如,你可以将 ASCII 控制字符 escape
的字节字面量写成 b'\x1b'
,因为 escape
的 ASCII 码为 27,即十六进制的 1B。由于字节字面量只是 u8
值的表示法之一,因此还应该考虑使用一个整型字面量是否更易读:只有当你要强调该值表示的是 ASCII 码时,才应该使用 b'\x1b'
而不是简单明了的 27。
可以使用 as
运算符将一种整型转换为另一种整型。6.14 节会详细讲解类型转换的原理,这里先举一些例子:
assert_eq!( 10_i8 as u16, 10_u16); // 范围内转换 assert_eq!( 2525_u16 as i16, 2525_i16); // 范围内转换 assert_eq!( -1_i16 as i32, -1_i32); // 带符号扩展 assert_eq!(65535_u16 as i32, 65535_i32); // 填零扩展 // 超出目标范围的转换生成的值等于原始值对2N取模的值, // 其中N是按位算的目标宽度。有时这也称为“截断” assert_eq!( 1000_i16 as u8, 232_u8); assert_eq!(65535_u32 as i16, -1_i16); assert_eq!( -1_i8 as u8, 255_u8); assert_eq!( 255_u8 as i8, -1_i8);
标准库提供了一些运算,可以像整型的方法一样使用。例如:
assert_eq!(2_u16.pow(4), 16); // 求幂 assert_eq!((-4_i32).abs(), 4); // 求绝对值 assert_eq!(0b101101_u8.count_ones(), 4); // 求二进制1的个数
可以在在线文档中找到这些内容。但请注意,该文档在“i32
(原始类型)”和此类型的专有模块(搜索“std::i32
”)下的单独页面中分别含有此类型的信息。
在实际编码中,通常不必像刚才那样写出类型后缀,因为其上下文将决定类型。但是,如果没有类型后缀且无法决定类型,那么错误消息可能会令人惊讶。例如,以下代码无法编译:
println!("{}", (-4).abs());
Rust 会报错:
error: can't call method `abs` on ambiguous numeric type `{integer}`
这令人不解:明明所有的有符号整型都有一个 abs
方法,那么问题出在哪里呢?出于技术原因,Rust 在调用类型本身的方法之前必须确切地知道一个值属于哪种整型。只有在解析完所有方法调用后类型仍然不明确的时候,才会默认为 i32
,但这里并没有其他方法可供解析,因此 Rust 提供不了帮助。解决方案是加上后缀或使用特定类型的函数来明确写出希望的类型:
println!("{}", (-4_i32).abs()); println!("{}", i32::abs(-4));
请注意,方法调用的优先级高于一元前缀运算符,因此在将方法应用于负值时要小心。如果第一个语句中 -4_i32
周围没有圆括号,则 -4_i32.abs()
会先针对正值 4
调用 abs
方法,生成正值 4
,再根据负号取负,得到 -4
。
3.1.2 检查算法、回绕算法、饱和算法和溢出算法
当整型算术运算溢出时,Rust 在调试构建中会出现 panic。而在发布构建中,运算会回绕:它生成的值等于“数学意义上正确的结果”对“值类型范围”取模的值。(在任何情况下都不会像 C 和 C++ 中那样出现“溢出未定义”的行为。)
例如,以下代码在调试构建中会出现 panic:
let mut i = 1; loop { i *= 10; // panic:试图进行可能溢出的乘法(但只会在调试构建中出现) }
在发布构建中,此乘法会返回负数,并且循环会无限运行。
如果这种默认行为不是你想要的,则整型提供的某些方法可以让你准确地阐明自己期望的行为。例如,在任意构建中都会出现下列 panic:
let mut i = 1; loop { i *= 10; // panic:试图进行可能溢出的乘法(但只会在调试构建中出现) }
在发布构建中,此乘法会返回负数,并且循环会无限运行。
如果这种默认行为不是你想要的,则整型提供的某些方法可以让你准确地阐明自己期望的行为。例如,在任意构建中都会出现下列 panic:
let mut i: i32 = 1; loop { // panic:乘法溢出(在任意构建中出现) i = i.checked_mul(10).expect("multiplication overflowed"); }
这些整型算术方法分为 4 大类。
- 检查运算会返回结果的
Option
值:如果数学意义上正确的结果可以表示为该类型的值,那么就为Some(v)
,否则为None
。
// 10与20之和可以表示为u8 assert_eq!(10_u8.checked_add(20), Some(30)); // 很遗憾,100与200之和不能表示为u8 assert_eq!(100_u8.checked_add(200), None); // 做加法。如果溢出,则会出现panic let sum = x.checked_add(y).unwrap(); // 奇怪的是,在某种特殊情况下,带符号的除法也会溢出。 // 带符号的n位类型可以表示-2n-1,但不足以表示2n-1 assert_eq!((-128_i8).checked_div(-1), None);
回绕运算会返回与“数学意义上正确的结果”对“值类型范围”取模的值相等的值。
// 第一个结果可以表示为u16,第二个则不能,所以会得到250000 对216的模 assert_eq!(100_u16.wrapping_mul(200), 20000); assert_eq!(500_u16.wrapping_mul(500), 53392); // 对有符号类型的运算可能会回绕为负值 assert_eq!(500_i16.wrapping_mul(500), -12144); // 在移位运算中,移位距离会在值的大小范围内回绕, // 所以在16位类型中移动17位就相当于移动了1位 assert_eq!(5_i16.wrapping_shl(17), 10);
如前所述,这就是普通算术运算符在发布构建中的行为。这些方法的优点是它们在所有构建中的行为方式都是相同的。
- 饱和运算会返回最接近“数学意义上正确结果”的可表达值。换句话说,结果“紧贴着”该类型可表达的最大值和最小值。
assert_eq!(32760_i16.saturating_add(10), 32767); assert_eq!((-32760_i16).saturating_sub(10), -32768);
- 不存在饱和除法1、饱和求余法2或饱和位移法3。
- 溢出运算会返回一个元组
(result, overflowed)
,其中result
是函数的回绕版本所返回的内容,而overflowed
是一个布尔值,指示是否发生过溢出。
assert_eq!(255_u8.overflowing_sub(2), (253, false)); assert_eq!(255_u8.overflowing_add(2), (1, true));
overflowing_shl
和 overflowing_shr
稍微偏离了这种模式:只有当移位距离与类型本身的位宽一样大或比其更大时,它们才会为 overflowed
返回 true
。实际应用的移位数是所请求的移位数对类型位宽取模的结果。
// 移动17位对`u16`来说太大了,而17对16取模就是1 assert_eq!(5_u16.overflowing_shl(17), (10, true));
1整数相除一般不会溢出,即使溢出也没有“数学意义上的正确结果”。——译者注
2饱和是对溢出的一种补救方式,余数不可能溢出,因此饱和也没有意义。——译者注
3饱和是对溢出的一种补救方式,移位的溢出在不同情况下补救方式不同,因此无法统一支持。——译者注
前缀 checked_
、wrapping_
、saturating_
或 overflowing_
后面可以跟着的运算名称如表 3-7 所示。
表 3-7:运算名称
运算 | 名称后缀 | 例子 |
加法 | add |
100_i8.checked_add(27) == Some(127) |
减法 | sub |
10_u8.checked_sub(11) == None |
乘法 | mul |
128_u8.saturating_mul(3) == 255 |
除法 | div |
64_u16.wrapping_div(8) == 8 |
求余 | rem |
(-32768_i16).wrapping_rem(-1) == 0 |
取负 | neg |
(-128_i8).checked_neg() == None |
绝对值 | abs |
(-32768_i16).wrapping_abs() == -32768 |
求幂 | pow |
3_u8.checked_pow(4) == Some(81) |
按位左移 | shl |
10_u32.wrapping_shl(34) == 40 |
按位右移 | shr |
40_u64.wrapping_shr(66) == 10 |
3.1.3 浮点类型
Rust 提供了 IEEE 单精度浮点类型和 IEEE 双精度浮点类型。这些类型包括正无穷大和负无穷大、不同的正零值和负零值,以及非数值。如表 3-8 所示。
表 3-8:IEEE 单精度浮点类型和 IEEE 双精度浮点类型
类型 | 精度 | 范围 |
f32 |
IEEE 单精度(至少 6 位小数) | 大约 -3.4 × 1038 至 +3.4 × 1038 |
f64 |
IEEE 双精度(至少 15 位小数) | 大约 -1.8 × 10308 至 +1.8 × 10308 |
Rust 的 f32
和 f64
分别对应于 C 和 C++(在支持 IEEE 浮点的实现中)以及 Java(始终使用 IEEE 浮点)中的 float
类型和 double
类型。
图 3-1:浮点字面量
浮点数中整数部分之后的每个部分都是可选的,但必须至少存在小数部分、指数或类型后缀这三者中的一个,以将其与整型字面量区分开来。小数部分可以仅由一个单独的小数点组成,因此 5.
也是有效的浮点常量。
如果浮点字面量缺少类型后缀,那么 Rust 就会检查上下文以查看值的使用方式,这与整型字面量非常相似。如果它最终发现这两种浮点类型都适合,就会默认选择 f64
。
为了便于类型推断,Rust 会将整型字面量和浮点字面量视为不同的大类:它永远不会把整型字面量推断为浮点类型,反之亦然。表 3-9 展示了浮点字面量的一些示例。
表 3-9:浮点字面量的例子
字面量 | 类型 | 数学值 |
-1.5625 |
自动推断 | −(19/16) |
2. |
自动推断 | 2 |
0.25 |
自动推断 | ¼ |
1e4 |
自动推断 | 10 000 |
40f32 |
f32 |
40 |
9.109_383_56e-31f64 |
f64 |
大约 9.109 383 56 × 10-31 |
f32
类型和 f64
类型具有 IEEE 要求的一些特殊值的关联常量,比如 INFINITY
(无穷大)、NEG_INFINITY
(负无穷大)、NAN
(非数值)以及 MIN
(最小有限值)和 MAX
(最大有限值):
assert!((-1. / f32::INFINITY).is_sign_negative()); assert_eq!(-f32::MIN, f32::MAX);
f32
类型和 f64
类型提供了完备的数学计算方法,比如 2f64.sqrt()
就是 2
的双精度平方根。下面是一些例子:
assert_eq!(5f32.sqrt() * 5f32.sqrt(), 5.); // 按IEEE的规定,它精确等于5.0 assert_eq!((-1.01f64).floor(), -2.0);
再次提醒,方法调用的优先级高于前缀运算符,因此在对负值进行方法调用时,请务必正确地加上圆括号。
std::f32::consts
模块和 std::f64::consts
模块提供了各种常用的数学常量,比如 E
、PI
和 2
的平方根。
在搜索文档时,请记住这两种类型本身都有名为“f32
(原始类型)”和“f64
(原始类型)”的页面,以及每种类型的单独模块 std::f32
和 std::f64
。
与整数一样,通常不必在实际代码中写出浮点字面量的类型后缀,但如果你想这么做,那么将类型放在字面量或函数上就可以:
println!("{}", (2.0_f64).sqrt()); println!("{}", f64::sqrt(2.0));
与 C 和 C++ 不同,Rust 几乎不会执行任何隐式的数值转换。如果函数需要 f64
参数,则传入 i32
型参数是错误的。事实上,Rust 甚至不会隐式地将 i16
值转换为 i32
值,虽然每个 i16
值都必然在 i32
范围内。不过,你随时可以用 as
运算符写出显式转换:i as f64
或 x as i32
。
缺少隐式转换有时会让 Rust 表达式比类似的 C 或 C++ 代码更冗长。然而,隐式整数转换有着导致错误和安全漏洞的大量“前科”,特别是在用这种整数表示内存中某些内容的大小时,很可能发生意外溢出。根据以往的经验,Rust 这种要求明确写出数值类型转换的行为,会提醒我们注意到一些可能错过的问题。