数据类型:标量类型与复合类型
数据类型这东西,说白了就是告诉编译器「我要存什么样的数据」。Rust 是静态类型语言,每个变量在编译时就必须知道自己的类型。嗯,这一点跟 Python 那种动态类型不一样,刚开始写 Rust 时我确实不太习惯。
我个人习惯把 Rust 的数据类型分成两大类:标量类型和复合类型。咱们一个一个来看。
标量类型:最基础的数据单元
标量类型代表单个值。Rust 有四种标量类型:整数、浮点、布尔、字符。
整数类型
整数类型分两种:有符号(i 开头)和无符号(u 开头)。有符号可以表示负数,无符号只能表示正数。
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8 位 | i8 | u8 |
| 16 位 | i16 | u16 |
| 32 位 | i32 | u32 |
| 64 位 | i64 | u64 |
| 128 位 | i128 | u128 |
| arch | isize | usize |
默认的整数类型是 i32。为什么?因为它在大多数 CPU 上效率最高。我在项目中处理过嵌入式开发,那时候就得用 u8 或 u16 来节省内存。
整数溢出问题
Rust 在 debug 模式下会检查整数溢出,一旦溢出就 panic。release 模式下则采用「环绕」处理。我曾经在写一个计数器时踩过这个坑——debug 跑得好好的,release 就出诡异问题。后来我养成了习惯:明确使用 wrapping_add、saturating_add 这类方法。
let a: u8 = 255;
// let b = a + 1; // debug 模式下会 panic
let b = a.wrapping_add(1); // 结果为 0
let c = a.saturating_add(1); // 结果为 255
浮点类型
Rust 有两种浮点类型:f32 和 f64。默认是 f64,因为现代 CPU 处理 64 位浮点数的速度和 32 位差不多,但精度更高。
let x = 2.0; // f64
let y: f32 = 3.0; // f32
你想想看,什么时候用 f32?我在做游戏开发时,3D 坐标用 f32 就够了,能省一半内存。但做科学计算,我建议用 f64,精度差一点结果可能差很多。
浮点数比较要小心
浮点数不能直接用 == 比较。因为精度问题,0.1 + 0.2 不一定等于 0.3。我曾经在金融系统里看到过这种 bug——两个浮点数明明「应该」相等,但比较结果却是 false。解决方案是用 (a - b).abs() < epsilon 这种方式。
布尔类型
布尔类型就两个值:true 和 false。大小是一个字节。
let is_ok: bool = true;
let is_not = false;
嗯,这里要注意:Rust 不会自动把非布尔值转换成布尔值。像 C 语言里 if (1) 这种写法,在 Rust 里是编译错误的。必须写成 if x != 0。
字符类型
Rust 的 char 类型很有意思。它占 4 个字节,代表一个 Unicode 标量值。这意味着它可以表示中文、日文、emoji 等等。
let c = 'z';
let z = '中';
let emoji = '😊';
我记得刚开始学 Rust 时,以为 char 跟 C 语言一样是 1 个字节。结果发现完全不是一回事。Rust 的 char 是 4 字节,能表示所有 Unicode 字符。但如果你要处理 ASCII 文本,用 u8 更省内存。
复合类型:把多个值组合在一起
复合类型可以把多个值打包成一个整体。Rust 有两种基本的复合类型:元组和数组。
元组(Tuple)
元组可以把不同类型的值组合在一起。长度固定,一旦声明就不能改变。
let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
println!("y = {}", y); // 6.4
访问元组元素有两种方式:解构或者用点号加索引。
let tup = (500, 6.4, 1);
let first = tup.0; // 500
let second = tup.1; // 6.4
我在项目中常用元组来返回多个值。比如一个函数要返回状态码和错误信息,用元组就很方便。
元组的实际用途
元组特别适合「临时组合」几个值。比如从函数返回多个结果,或者把几个相关的值暂时绑在一起。但如果数据量大了,或者需要频繁访问,我建议还是用结构体。
数组(Array)
数组跟元组类似,长度也是固定的。但数组里所有元素的类型必须相同。
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let first = arr[0]; // 1
还有一种简洁写法:
let arr = [3; 5]; // [3, 3, 3, 3, 3]
数组的索引从 0 开始。如果访问越界,Rust 会在运行时 panic。这一点跟 C 语言不一样——C 语言越界访问不会报错,但可能踩到别人的内存。Rust 帮你兜底了。
数组越界检查
Rust 会在运行时检查数组索引是否越界。如果你写 arr[10] 而数组只有 5 个元素,程序会直接 panic 退出。我曾经在写循环时少写了一个边界条件,结果 debug 时立刻就被抓住了。嗯,虽然当时觉得烦,但事后想想,这比 C 语言那种「静默崩溃」好太多了。
类型推断与类型注解
Rust 编译器很聪明,它能根据上下文推断出变量的类型。但有时候也需要你明确告诉它。
let guess = "42".parse().expect("Not a number"); // 编译错误!
为什么?因为 parse() 可以返回多种类型,编译器不知道你想要什么。这时候就需要类型注解:
let guess: u32 = "42".parse().expect("Not a number"); // 正确
或者用「涡轮鱼」语法:
let guess = "42".parse::<u32>().expect("Not a number");
我个人习惯用第一种方式,更直观。但涡轮鱼语法在某些场景下确实好用,比如链式调用时。
类型推断的边界
Rust 的类型推断是「局部」的。它不会像 Haskell 那样做全局类型推断。所以有时候你得多写几个类型注解。刚开始可能觉得麻烦,但习惯了就会发现——这其实是在帮你减少 bug。
知识体系总览
下面这张图把本章的数据类型体系梳理了一下,方便你对照着看:
这张图把标量类型和复合类型的关系画得很清楚。左边是四种标量类型,右边是两种复合类型。底部是类型推断与注解——这是贯穿所有类型的「元技能」。
好了,数据类型这块就聊到这儿。记住一句话:Rust 的类型系统不是来限制你的,而是来保护你的。刚开始写的时候可能会觉得「怎么这么啰嗦」,但等你写出一个没有段错误、没有空指针的程序时,你就会感谢它了。
公众号:蓝海资料掘金营,微信deep3321