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_addsaturating_addchecked_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 知识体系总览

下面这张图帮你理清标量数据类型的整体结构:

Rust 标量数据类型 整数类型 浮点类型 布尔类型 字符类型 有符号: i8, i16, i32, i64, i128 无符号: u8, u16, u32, u64, u128 默认: i32 | 注意溢出 f32 (单精度) f64 (双精度) 默认: f64 true / false 占 1 字节 无隐式转换 Unicode 标量 占 4 字节 支持 emoji 选择建议 整数默认 i32 | 浮点默认 f64 | 布尔用 bool | 字符用 char 内存敏感时再考虑 u8、f32 等更小类型

3.6 选择建议总结

说了这么多,最后给个实用建议:

  • 整数:默认 i32。如果数据范围明确且小,用 u8/i8。如果可能很大,用 i64。i128 除非你真的需要 128 位精度。
  • 浮点:默认 f64。只有存大量数据时才考虑 f32。
  • 布尔:没得选,就是 bool。
  • 字符:处理单个 Unicode 字符用 char。处理文本用 &str 或 String。

我的经验:刚开始学 Rust 时,我总喜欢用最宽的类型,觉得这样最安全。后来发现,类型选对了,代码不仅跑得快,而且 bug 也少。比如用 u8 表示月份,编译器就能帮你检查出 13 月这种错误。

好了,标量类型就聊到这儿。记住一句话:类型是 Rust 给你的第一道防线。选对了,后面省心很多。


专注资料整理