小伙伴们,当我们谈论系统编程时,"模糊"是万恶之源。一个在 32 位系统上运行正常的 int,可能在 64 位系统上引发灾难。Rust 从源头上解决了这个问题:它的基本类型(也称标量类型)具有固定且明确的大小 。
1. 整数 (Integers):不只是数字,更是"契约"
Rust 提供了非常精细的整数类型家族。
-
有符号 (Signed):
i8,i16,i32,i64,i128(i代表 integer) -
无符号 (Unsigned):
u8,u16,u32,u64,u128(u代表 unsigned)
数字代表它们占用的位数(bits)。例如,u8 是一个 8 位无符号整数,范围是 0 到 255。i8 是一个 8 位有符号整数,范围是 -128 到 127。
专业思考:
为什么要这么麻烦?
-
内存精确性: 如果你知道你的数据(比如一个人的年龄)永远不会超过 200,使用
u8(1 字节) 而不是i32(4 字节) 或i64(8 字节)。在处理数百万个这样的数据时,节省的内存是惊人的。 -
明确的意图: 当你解析网络协议或读取二进制文件时,协议会精确定义"接下来的 4 个字节是一个无符号 32 位整数"。在 Rust 中,你直接使用
u32,意图清晰,绝无歧义。 -
isize和usize: 这两个"架构相关"类型是 Rust 的点睛之笔。它们的大小取决于你的 CPU 架构(32 位系统上是 32 位,64 位系统上是 64 位)。usize为什么重要? 因为它被设计用来"索引"集合(如Vec或数组)。一个集合能放多少元素,取决于内存地址空间有多大。usize完美匹配了这一点。
实践深潜:整数溢出 (Integer Overflow) 的"优雅处理"
这是 C/C++ 开发者永远的痛------整数溢出(比如 u8 的 255 + 1)在 C 中是"未定义行为"(UB),是无数安全漏洞的根源。
Rust 怎么解决?它强迫你正视这个问题。
-
Debug 模式: 溢出时,程序会
panic!💥。 -
Release 模式: 溢出时,Rust 会执行二进制补码环绕 (Two's Complement Wrapping) (例如
(255_u8 + 1)会变成0)。
专业实践:
Rust 不希望你依赖隐Rust 不希望你依赖隐式的"环绕"。它提供了专门的 API 来让你明确表达你希望如何处理溢出:
let a: u8 = 250;
let b: u8 = 10;
// 1. 我"知道"它会溢出,我"想要"环绕
let wrapped_add = a.wrapping_add(b); // 结果是 4 (260 % 256)
println!("Wrapped: {}", wrapped_add);
// 2. 我"不确定"是否溢出,请"检查"
let checked_add = a.checked_add(b);
match checked_add {
Some(sum) => println!("Checked Sum: {}", sum), // 如果没溢出
None => println!("Overflowed!"), // 如果溢出了
}
// 3. 我希望溢出时"饱和"(停在最大/最小值)
let saturating_add = a.saturating_add(b); // 结果是 255 (u8::MAX)
println!("Saturated: {}", saturating_add);
// 4. 我想知道"是否"溢出了
let (overflowing_add, did_overflow) = a.overflowing_add(b);
println!("Overflowing: {}, Did overflow: {}", overflowing_add, did_overflow); // 4, true
思考: Rust 通过这套机制,把一个"运行时"的魔鬼(UB)变成了"编译期"或"运行时"可控的安全选择。
2. 浮点数 (Floating Point):f32 与 f64 的"陷阱"
Rust 提供了两种浮点数:f32 (单精度) 和 f64 (双精度)。
专业思考:
默认是 f64。为什么?因为在现代 64 位 CPU 上,f64 的计算速度和 f32 基本没差,但它提供了高得多的精度。Rust 默认选择了"更安全"、"更精确"的选项。
实践深潜:浮点数的"比较"与 NaN
所有使用 IEEE 754 浮点数标准的语言都有一个共同的"陷阱"。
let a = 0.1_f64;
let b = 0.2_f64;
let c = 0.3_f64;
// 警告! 🚨
if a + b == c {
println!("Equals!"); // 这行可能不会打印
} else {
println!("NOT Equals! {} vs {}", a + b, c); // 打印这个!
// 因为 a + b 可能是 0.30000000000000004
}
专业实践:
永远不要用 == 检查两个浮点数是否相等。你应该检查它们的差值是否在一个非常小(Epsilon)的范围内。
let diff = (a + b - c).abs();
assert!(diff < f64::EPSILON); // EPSILON 是 f64 能表示的最小差值
**更深的思考:`NaN Trait**
浮点数有一个特殊的值:NaN (Not a Number),比如 `(0.0 / 0.0) 的结果。NaN 有个古怪的特性:它不等于任何东西,包括它自己。
let nan = 0.0_f64 / 0.0;
// assert!(nan == nan); // 💥 这会 panic!
这个特性导致 Rust 做出一个重要的设计决策:f32 和 f64 没有实现 Eq 和 Ord Trait 。(Eq 要求 a == a 必须为 true)。
这意味着什么?
-
你不能在一个
BTreeSet(要求Ord) 中存放浮点数。 -
你不能(默认)在一个
HashMap(要求Eq和Hash) 中使用浮点数作为 Key。
这个限制是故意 的,Rust 在保护你,防止你掉进 NaN 导致的逻辑陷阱里。
3. 字符 (char) 与布尔 (bool):小类型,大学问
-
**
bool(值):**true或false。- 专业思考:
bool占用 1 个字节 (u8)。为什么不是 1 位?因为 CPU 无法高效地寻址单个"位"。为了内存对齐 (Memory Alignment) ,Rust 选择了了 1 字节作为bool的最小尺寸,用空间换取了速度。
- 专业思考:
-
char(字符):* * 专业思考: 这是 Rust 和 C/C++ 最大的区别之一!
-
C/C++ 的
char是 1 字节,它只能表示 ASCII。 -
Rust 的
char是 4 字节 (32位),它代表一个 **Unicode量值 (Unicode Scalar Value)**。
-
实践深潜:char vs &str
Rust 的 char 可以表示任何合法的 Unicode 字符,包括中文、日文、甚至 Emoji。
let c1 = 'a';
let c2 = '中';
let c3 = '🚀';
// 所有 char 都是 4 字节
println!("Size of 'a': {}", std::mem::size_of_val(&c1)); // 4
println!("Size of '中': {}", std::mem::size_of_val(&c2)); // 4
println!("Size of '🚀': {}", std::mem::size_of_val(&c3)); // 4
但是,字符串 &str 呢?Rust 的字符串使用 UTF-8 编码,是可变长度的。
let s1 = "a";
let s2 = "中";
let s3 = "🚀";
// UTF-8 编码的字节长度
println!("Byte length of 'a': {}", s1.len()); // 1
println!("Byte length of '中': {}", s2.len()); // 3
println!("Byte length of '🚀': {}", s3.len()); // 4
思考: Rust 在语言层面**强制你区分"字符"(一个 Unicode 标量)和"字符串的字节表示"(UTF-8)。char 总是 4 字节,这让编译器能进行优化;而 &str 采用高效的 UTF-8 存储。这种设计,让 Rust 在处理全球化文本时,既安全又高效。
总结
Rust 的基本数据类型,体现了它对精确性、安全性和性能的极致追求。
-
整数: 强迫你处理溢出,提供了精细的内存控制。
-
浮点数: 强迫你注意比较陷阱,并通过 Trait 限制防止你误用
NaN。 -
字符/布尔: 在内存对齐和 Unicode 正确性上做出了最优选择。
掌握了这些,你才真正拿到了精确操控内存的"钥匙"!