3、标量数据类型:从 i8 到 i128,你真的选对了吗?
聊到 Rust 的类型系统,我第一个想说的就是标量类型。说白了,标量就是单个值,不像元组或数组那样能装一堆东西。Rust 的标量类型有四种:整数、浮点数、布尔值和字符。今天咱们一个一个过,顺便聊聊我这些年踩过的坑。
3.1 整数类型:i8/u8 到 i128/u128
Rust 的整数类型分有符号(i 开头)和无符号(u 开头)。有符号能表示负数,无符号只能表示正数和零。数字代表位数,比如 i8 是 8 位有符号整数,范围是 -128 到 127。u8 是 8 位无符号整数,范围是 0 到 255。
你可能会问:为什么搞这么多类型?直接用一个通用的不行吗?嗯,这里有个关键点——内存效率和性能。我在做嵌入式项目时,一个传感器返回的数据范围就是 0-100,用 u8 就够了。你非要用 i128,不仅浪费内存,还可能让 CPU 多干活。
| 类型 | 位数 | 最小值 | 最大值 |
|---|---|---|---|
| i8 | 8 | -128 | 127 |
| u8 | 8 | 0 | 255 |
| i16 | 16 | -32768 | 32767 |
| u16 | 16 | 0 | 65535 |
| i32 | 32 | -2147483648 | 2147483647 |
| u32 | 32 | 0 | 4294967295 |
| i64 | 64 | -2^63 | 2^63-1 |
| u64 | 64 | 0 | 2^64-1 |
| i128 | 128 | -2^127 | 2^127-1 |
| u128 | 128 | 0 | 2^128-1 |
核心建议:默认用 i32。除非你明确知道数据范围很小(比如用 u8),或者需要处理超大数值(比如用 i64/i128)。别一上来就 i128,那是给自己找麻烦。
3.1.1 整数溢出:一个让我吃过亏的问题
在 Rust 中,整数溢出在 debug 模式下会 panic,在 release 模式下会回绕(wrapping)。什么意思?举个例子:
let x: u8 = 255;
let y = x + 1; // debug 模式:panic!release 模式:y = 0
我曾经在写一个计数器时,没注意 u8 的上限。测试环境跑得好好的,一上线就崩了。后来才发现是 debug 和 release 行为不一致导致的。所以我的习惯是:如果可能溢出,用 wrapping_add、saturating_add 或 checked_add 明确处理。
let x: u8 = 255;
let y = x.wrapping_add(1); // y = 0,明确回绕
let z = x.saturating_add(1); // z = 255,停在最大值
避坑指南:我曾经在金融计算中用了 u32,结果金额累加时溢出了。从那以后,涉及金额或计数的场景,我至少用 u64 起步。
3.2 浮点类型:f32 和 f64
Rust 只有两种浮点类型:f32(单精度)和 f64(双精度)。默认是 f64,因为现代 CPU 对 f64 的支持已经很好,精度更高,性能差距不大。
我个人习惯:除非你明确需要节省内存(比如存几百万个坐标点),否则就用 f64。为什么?因为 f32 的精度只有大约 7 位有效数字,而 f64 有 15-16 位。你算个三角函数,f32 可能就给你四舍五入到不知道哪去了。
let a: f32 = 0.1 + 0.2;
let b: f64 = 0.1 + 0.2;
println!("f32: {}", a); // 0.30000001192092896
println!("f64: {}", b); // 0.30000000000000004
看到了吗?两个都不精确。但 f64 更接近真实值。浮点数本身就有精度问题,这是 IEEE 754 标准决定的,不是 Rust 的锅。
小技巧:比较浮点数时别用 ==。用 (a - b).abs() < 1e-10 这种形式。我见过太多人因为直接比较浮点数而 debug 到崩溃。
3.3 布尔类型:bool
布尔类型就两个值:true 和 false。占 1 个字节。没什么好说的,但有个细节:Rust 不会自动把非布尔值当成布尔值用。比如 C 语言里 if (1) 是合法的,Rust 不行。
let flag = true;
if flag {
println!("这是真的");
}
// 下面这行编译不过
// if 1 {
// println!("C 语言可以,Rust 不行");
// }
我觉得这个设计挺好的。少了很多隐式转换带来的 bug。你想想看,if (x = 5) 这种经典错误在 Rust 里根本不会出现。
3.4 字符类型:char
Rust 的 char 类型是 4 字节的 Unicode 标量值。注意,不是 ASCII,是 Unicode。这意味着它可以表示任何语言的字符,包括 emoji。
let c1: char = 'A';
let c2: char = '中';
let c3: char = '😊';
println!("{}{}{}", c1, c2, c3); // A中😊
这里有个容易混淆的点:char 是 4 字节,但字符串(&str)中的字符是 UTF-8 编码的,每个字符占 1-4 字节不等。所以 char 和 字符串中的字符 不是一回事。
记住:char 是 4 字节的 Unicode 值。如果你要处理的是纯 ASCII 文本,用 u8 更省内存。但如果你要处理国际化文本,char 是你的好朋友。
3.5 知识体系总览
下面这张图帮你理清标量数据类型的整体结构:
3.6 选择建议总结
说了这么多,最后给个实用建议:
- 整数:默认 i32。如果数据范围明确且小,用 u8/i8。如果可能很大,用 i64。i128 除非你真的需要 128 位精度。
- 浮点:默认 f64。只有存大量数据时才考虑 f32。
- 布尔:没得选,就是 bool。
- 字符:处理单个 Unicode 字符用 char。处理文本用 &str 或 String。
我的经验:刚开始学 Rust 时,我总喜欢用最宽的类型,觉得这样最安全。后来发现,类型选对了,代码不仅跑得快,而且 bug 也少。比如用 u8 表示月份,编译器就能帮你检查出 13 月这种错误。
好了,标量类型就聊到这儿。记住一句话:类型是 Rust 给你的第一道防线。选对了,后面省心很多。