数据类型:标量类型与复合类型

数据类型这东西,说白了就是告诉编译器「我要存什么样的数据」。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 上效率最高。我在项目中处理过嵌入式开发,那时候就得用 u8u16 来节省内存。

整数溢出问题

Rust 在 debug 模式下会检查整数溢出,一旦溢出就 panic。release 模式下则采用「环绕」处理。我曾经在写一个计数器时踩过这个坑——debug 跑得好好的,release 就出诡异问题。后来我养成了习惯:明确使用 wrapping_addsaturating_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 有两种浮点类型:f32f64。默认是 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 这种方式。

布尔类型

布尔类型就两个值:truefalse。大小是一个字节。

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 数据类型 标量类型(Scalar) 复合类型(Compound) 整数 (i32, u8...) 浮点 (f32, f64) 布尔 (bool) 字符 (char) 元组 (Tuple) 数组 (Array) 类型推断与注解 • 编译器能推断大部分类型,但复杂场景需要显式注解 • 类型注解:let x: u32 = 42; • 涡轮鱼语法:let x = "42".parse::<u32>();

这张图把标量类型和复合类型的关系画得很清楚。左边是四种标量类型,右边是两种复合类型。底部是类型推断与注解——这是贯穿所有类型的「元技能」。

好了,数据类型这块就聊到这儿。记住一句话:Rust 的类型系统不是来限制你的,而是来保护你的。刚开始写的时候可能会觉得「怎么这么啰嗦」,但等你写出一个没有段错误、没有空指针的程序时,你就会感谢它了。


公众号:蓝海资料掘金营,微信deep3321