基本数据类型:标量、元组与数组
类型系统是 Rust 编译器干活的地基,而基本数据类型是地基最底下那一层。这篇解决的问题是:整数和浮点各有哪些宽度、什么时候必须手写类型标注、元组和数组能装什么、类型转换的坑在哪里。读完你写声明与函数签名时不会再犹豫该用 i32 还是 u64,也知道为什么编译器有时候突然问你「你到底想要什么类型」。
一、标量类型一览
Rust 的标量类型分四族:整数、浮点、布尔、字符。整数按位宽和符号组合成十二种,初学者不用全背,记住最常用的几种即可。
| 类型 | 含义 | 典型用途 |
|---|---|---|
i32 |
有符号 32 位 | 默认整数类型 |
u32 |
无符号 32 位 | 长度、数量 |
i64 / u64 |
64 位 | 大数、时间戳 |
isize / usize |
平台位宽 | 索引、大小 |
f64 |
双精度浮点 | 默认浮点类型 |
bool |
布尔 | 条件判断 |
char |
4 字节 Unicode 标量 | 单个字符 |
两个「默认」值得强调:写下 let n = 42; 时编译器默认按 i32 推断,写下 let x = 2.0; 时默认按 f64 推断。数组下标与容器容量相关的地方默认是 usize,和平台位宽一致,这是它和 C 语言里 size_t 同源的设定。选型时的实用建议是:没有特殊理由就用 i32,它是 CPU 指令上最泛用的宽度;涉及索引、长度、容量时用 usize,避免和标准库 API 反复互转;只有明确知道数值范围时才换 u64 之类,否则类型转换会淹没你的代码。
整数类型可以从字面量读出进制:0x 前缀十六进制、0o 八进制、0b 二进制,数字间还允许用下划线分段,比如 1_000_000。这些写法纯粹是给人看的,编译器一视同仁。
二、类型推断与必须标注的场合
Rust 的类型推断相当聪明,但不会跨越函数边界:函数签名必须显式写出参数与返回类型,这是为了让人在不读函数体的情况下就能信赖接口。编辑器里有了 rust-analyzer 之后,悬停即可看到推断出的类型,不必靠猜。
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add(1, 2); // 推断为 i32
let flag: bool = true; // 显式标注
}
有一种新手必遇的恍惚时刻:let guess: i32 = "42".parse().unwrap(); 里 parse 的泛型目标由冒号左侧的标注决定,去掉标注编译器就报「类型不明确」让你自己写清楚。这类错误通常长成 cannot infer type 或 expected one of ... 的句式,本质是编译器在多个候选间无法选择。遇到它就在声明处补上类型标注,几乎总是正确动作。
三、整数溢出与转换
整数在 debug 构建里溢出会 panic,release 构建则按回绕处理——这是被反复强调的安全设计。想明确表达意图,标准库给了三个方法而不是让你默认接受回绕。
| 方法 | 溢出行为 | 使用场景 |
|---|---|---|
checked_add |
返回 None | 输入不可信时 |
wrapping_add |
回绕 | 明确要回绕语义 |
saturating_add |
饱和到边界 | 不允许越界 |
类型之间的转换用 as 关键字,比如 let big: i64 = 3; let small = big as i32;。as 是静默的、可能截断的强转,只在数值类型之间可用。想安全转换可以用 TryFrom / TryInto,转换失败返回 Result 而不是悄悄丢精度。处理用户数据或外部协议字段时,优先用可失败转换,把截断风险显式摊到台面上。
浮点数的坑和其他语言一样:f64 的比较要小心精度误差,NaN 不等于任何值包括自身。Rust 的除零在浮点上返回无穷大,在整型上则是 panic,两族语义不同,别混用。
整数类型的另一个现实问题是与外部系统对接时的宽度选择:网络协议字段常常是固定字节宽度,DTO 里照着协议写 u16、i32 即可,不要为了「反正能装下」统一用 64 位,那会让阅读者误以为字段很大。拿不准外部边界时,写注释说明取值范围,比类型本身更顶用。
四、元组:固定长度的异构容器
元组把几个可能类型不同的值打包成一个,长度固定,编译期已知。它在函数「返回多个值」时最常见:
fn split_url(url: &str) -> (&str, &str) {
let idx = url.find("://").unwrap_or(0);
(&url[..idx], &url[idx + 3..])
}
fn main() {
let (scheme, rest) = split_url("https://example.com");
println!("{} | {}", scheme, rest);
}
访问元组元素用 t.0、t.1 这种点号索引,从 0 开始。空元组 () 是一个独立类型,也叫单元类型,没有返回值的函数实际上就是返回了它。元组可以用解构语法一次性拆开,比逐个点索引清晰,模式匹配一章会见到更多解构的例子。超过三四层嵌套的元组就该考虑换成具名 struct 了——读者面对 (String, Vec<i32>, Option<u32>, bool) 这种签名时,基本要靠猜才能记住每个位置的含义,而 struct 的字段名是自解释的。函数返回多个值时,若顺序有歧义,宁愿定义一个小结构体。
五、数组:栈上的定长序列
数组和元组一样长度固定,但所有元素类型必须相同,且分配在栈上,访问开销接近零。数组适合编译期已知长度的场景,比如一星期七天、RGB 三个通道:
fn main() {
let weekdays = ["周一", "周二", "周三", "周四", "周五"];
let rgb: [u8; 3] = [255, 128, 0];
println!("红色通道: {}", rgb[0]);
println!("工作日数量: {}", weekdays.len());
}
越界访问在运行时 panic 而不是返回垃圾值。想自己处理越界,用 rgb.get(5) 拿到 Option<&u8>,配合模式匹配走可恢复路径。数组与 Vec 的分工是本节最重要的选型:长度编译期确定用数组,需要动态增删用 Vec。下表把两者的差别先摆清楚,集合实战一章再展开 Vec。
| 维度 | 数组 [T; N] |
Vec<T> |
|---|---|---|
| 长度 | 编译期固定 | 运行时可变 |
| 存储 | 栈 | 堆 |
| 越界访问 | panic 或 Option | 同左 |
| 典型场景 | 定长配置、缓冲区 | 任意动态集合 |
六、标量之外的字符与布尔细节
char 是 4 字节的 Unicode 标量值,不是字节也不是码元。这意味着一个中文字符在 char 里是一个元素,但在 String 里通常占三个 UTF-8 字节——字符串那一章会展开这个差异,这里先记住「char 不是 u8」。字节字面量写法是 b'A',类型是 u8,处理二进制协议时常用。
布尔就是 true / false,注意 if 条件必须是布尔类型,if x 这种写法在 Rust 里编译不过,不像某些语言会做隐式真值转换。这是类型系统刻意的严格:条件必须是人类可读的判断,而不是靠歧义约定。做配置解析时可以借鉴 正则表达式精进 里的做法,先把文本结构抓出来再转成强类型,这样类型错误在解析边界就被挡住,不会流进业务逻辑深处。
延伸阅读
- 下一节「字符串初识」把 String 与 &str 的区别讲透,是类型系统里最容易卡住新手的第二道坎。
- 字节、字符与编码的坑在 浏览器扩展开发指南 处理网页文本时同样常见,两边的经验可以互相迁移,读完会更清楚字符与字节的分界。
- 桌面端渲染文本时,字符宽度计算也有同样的坑,Tauri 桌面开发指南 的国际化章节值得一并参考。
- 想强化类型直觉,可以在本地把每段示例的
len()与字节数分别打印对照,眼见为实,这比记概念更牢固。
