第1章:数据类型——标量类型与复合类型
数据类型这东西,说白了就是告诉编译器「我要存什么样的数据」。Rust 是静态类型语言,每个变量在编译时就必须知道自己的类型。我刚开始学的时候觉得这很麻烦,后来才发现——这恰恰是 Rust 帮你避免无数运行时崩溃的底气所在。
1.1 标量类型:最基础的四种
标量类型代表一个单一的值。Rust 有四种标量类型:整数、浮点数、布尔、字符。咱们一个一个聊。
1.1.1 整数类型
整数分两种:有符号(i开头)和无符号(u开头)。长度从 8 位到 128 位,还有依平台而定的 isize 和 usize。
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
我个人习惯,默认用 i32。为什么?因为它在大多数 CPU 上效率最高。除非你明确知道需要更小或更大的范围,否则 i32 是最稳妥的选择。
核心要点:整数溢出在 debug 模式下会 panic,在 release 模式下会回绕。别指望它自动帮你处理边界。
let a: i8 = 127; // 最大值
let b: u32 = 4_294_967_295; // 可以用下划线分隔,方便阅读
let c = 0xff; // 十六进制
let d = 0o77; // 八进制
let e = 0b1111_0000; // 二进制
let f: u8 = b'A'; // 字节字面量
嗯,这里要注意:Rust 允许你在数字里加下划线。我写 100_000 比写 100000 舒服多了,一眼就能看出是十万。
1.1.2 浮点数类型
Rust 有两种浮点类型:f32 和 f64。默认是 f64,因为现代 CPU 处理 f64 和 f32 速度差不多,但精度更高。
let x = 2.0; // f64(默认)
let y: f32 = 3.0; // f32(显式标注)
我在项目中遇到过一个问题:用 f32 做累加,跑了十万次后误差累积到不可接受。换成 f64 就好了。所以,除非你明确需要节省内存(比如存大量传感器数据),否则用 f64。
注意:浮点数不支持 == 直接比较。因为 IEEE 754 标准下的精度问题,0.1 + 0.2 不一定等于 0.3。用 (a - b).abs() < epsilon 这种方式来比较。
1.1.3 布尔类型
布尔类型就两个值:true 和 false。大小是一个字节。
let is_ready: bool = true;
let is_done = false;
你想想看,别的语言里 0 可以当 false,非 0 可以当 true。Rust 不行。if 后面必须跟 bool 表达式。我曾经从 C++ 转过来时踩过这个坑——写了个 if (x) 结果编译报错。嗯,习惯了之后反而觉得这样更安全。
1.1.4 字符类型
Rust 的 char 是 4 字节的 Unicode 标量值。它不只是 ASCII,它能表示任何 Unicode 字符,包括 emoji。
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
这里有个坑:char 占 4 个字节,但字符串不是 char 的数组。字符串是 UTF-8 编码的,每个字符可能占 1 到 4 个字节。我刚开始写 Rust 时以为 String 就是 Vec<char>,结果被编译器教育了一顿。
1.2 复合类型:把多个值打包
复合类型能把多个值组合成一个。Rust 有两种原生复合类型:元组和数组。
1.2.1 元组(Tuple)
元组可以把不同类型的值打包在一起。长度固定,一旦声明就不能改变。
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
println!("y = {}", y); // 6.4
// 也可以用索引访问
let first = tup.0; // 500
let second = tup.1; // 6.4
我个人习惯用元组做函数返回多个值。比如一个函数既要返回结果码,又要返回数据,用元组就很方便。
fn divide(a: i32, b: i32) -> (i32, bool) {
if b == 0 {
(0, false)
} else {
(a / b, true)
}
}
let (result, ok) = divide(10, 3);
小技巧:元组可以嵌套。比如 (i32, (f64, char)) 完全合法。但别嵌套太深,否则代码可读性会下降。
1.2.2 数组(Array)
数组是相同类型元素的集合,长度固定。注意,Rust 的数组和 C 语言一样,长度是类型的一部分。
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = [3; 5]; // 等价于 [3, 3, 3, 3, 3]
let first = a[0]; // 1
let second = a[1]; // 2
数组访问越界会 panic。我在项目中遇到过:从网络接收数据后直接索引数组,结果对方发了个超长数据,程序直接崩溃。后来我改用 Vec 或加边界检查才解决。
警告:数组索引越界在 Rust 中会直接 panic,不会像 C 那样给你一个野指针。这是好事——崩溃总比安全漏洞强。
数组和元组的区别很明显:数组所有元素类型必须相同,元组可以不同。数组适合存储同质数据,元组适合存储异质数据。
1.3 知识体系总览
下面这张图帮你理清本章的知识结构:
1.4 类型推导与标注
Rust 能自动推导类型,但有时候需要你显式标注。比如:
let guess = "42".parse().expect("Not a number!"); // 编译错误!
编译器不知道你要解析成什么类型。这时候需要显式标注:
let guess: u32 = "42".parse().expect("Not a number!"); // 正确
我个人习惯在变量声明时尽量标注类型。虽然 Rust 能推导,但标注类型对代码可读性帮助很大。尤其是团队协作时,别人一眼就能看出你的意图。
建议:在函数签名和公开 API 中,永远显式标注类型。在局部变量中,如果类型很明显(比如 let x = 10),可以不标。
1.5 类型转换
Rust 不会做隐式类型转换。你想把 i32 转成 f64?必须显式写 as 关键字。
let a: i32 = 10;
let b: f64 = a as f64; // 显式转换
let c: u8 = 255;
let d: i8 = c as i8; // 小心!255 转成 i8 会变成 -1
我曾经在项目中犯过一个错:把一个 u32 的大数用 as 转成 i32,结果变成了负数。as 转换是直接截断位模式的,不会做范围检查。如果你需要安全的类型转换,用 TryFrom trait。
好了,数据类型就聊到这儿。记住一句话:Rust 的类型系统不是来为难你的,是来帮你的。刚开始觉得烦,写多了你就会发现——编译通过的那一刻,代码基本就是对的。
公众号:蓝海资料掘金营,微信deep3321