基本数据类型:标量、元组与数组

类型系统是 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() 与字节数分别打印对照,眼见为实,这比记概念更牢固。