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

下面这张图帮你理清本章的知识结构:

Rust 数据类型 标量类型 复合类型 整数 浮点 布尔 字符 元组 数组 标量类型:单一值 复合类型:多个值组合 整数:i8-u128, isize, usize 浮点:f32, f64 布尔:true / false 字符:4字节 Unicode

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