4. 基础数据类型:从整数到元组,Rust的类型世界

聊到Rust的数据类型,我脑子里第一个蹦出来的词是「严谨」。这语言在类型上真是下了狠功夫。你想想看,一个变量是整数还是浮点数,是32位还是64位,Rust都要你清清楚楚。刚开始可能觉得烦,但我在项目里吃过亏之后,才明白这种严谨有多香。

核心要点:Rust是静态强类型语言。所有变量在编译时就必须知道类型。这能帮你把很多运行时错误扼杀在摇篮里。

4.1 标量类型:最基础的数据单元

标量类型,说白了就是单个值。Rust有四种:整数、浮点数、布尔、字符。咱们一个一个来。

4.1.1 整数类型

Rust的整数类型分有符号(i开头)和无符号(u开头)。长度从8位到128位,还有根据机器位数决定的isize和usize。

长度 有符号 无符号
8位 i8 u8
16位 i16 u16
32位 i32 u32
64位 i64 u64
128位 i128 u128
架构相关 isize usize

我个人习惯,整数默认用i32。为什么?因为它在大多数CPU上效率最高。除非你有特殊需求,比如要存超大数字或者节省内存,否则i32够用了。

let a: i32 = 42;        // 显式声明
let b = -128i8;          // 字面量后缀
let c = 0xFF;            // 十六进制
let d = 0o77;            // 八进制
let e = 0b1010;          // 二进制
let f = 100_000;         // 下划线分隔,方便阅读

小技巧:数字里加下划线不会影响值,纯粹为了可读性。100_000和100000是一样的。我在写配置项时特别喜欢用这个。

4.1.2 浮点数类型

Rust只有两种浮点类型:f32和f64。默认是f64,因为现代CPU处理f64和f32速度差不多,但精度更高。

let x = 2.0;             // f64,默认
let y: f32 = 3.14;       // f32,显式声明
let z = 1.0e5;           // 科学计数法,等于100000.0

注意:浮点数不能直接比较相等。我在项目中遇到过因为浮点精度问题导致的bug,排查了好久。建议用 (a - b).abs() < f64::EPSILON 这种方式来比较。

4.1.3 布尔类型

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

let is_ok: bool = true;
let is_not = false;

if is_ok {
    println!("没问题");
}

嗯,这里要注意。Rust不会自动把非布尔值转成布尔。不像C语言里0就是false,非0就是true。Rust要求条件表达式必须是bool类型。

4.1.4 字符类型

Rust的char类型很有意思。它占4个字节,能表示Unicode标量值。也就是说,中文、日文、emoji都能存。

let c = 'z';
let z: char = '中';
let emoji = '😊';

我曾经在解析用户输入时,用char类型来处理多语言文本,效果很好。但要注意,char是4字节,如果你只是处理ASCII字符,用u8更省内存。

4.2 复合类型:把多个值打包在一起

复合类型,就是把多个值组合成一个整体。Rust提供了元组和数组两种基本复合类型。

4.2.1 元组

元组可以包含不同类型的值。长度固定,一旦声明就不能改变。

let tup: (i32, f64, char) = (42, 3.14, 'R');

// 解构
let (x, y, z) = tup;
println!("x = {}, y = {}, z = {}", x, y, z);

// 索引访问
let first = tup.0;
let second = tup.1;

元组在函数返回多个值时特别有用。我经常用它来返回状态码和数据。

fn get_user_info() -> (String, u32, bool) {
    (String::from("张三"), 28, true)
}

let (name, age, active) = get_user_info();

4.2.2 数组

数组和元组不同,它里面的元素必须是同一类型。长度也是固定的。

let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];  // 10个0

println!("第一个元素: {}", arr[0]);
println!("数组长度: {}", arr.len());

安全提示:Rust会在运行时检查数组越界。如果你访问arr[10],程序会直接panic。我曾经在写算法时忘了检查边界,结果程序崩溃了。后来养成了用get()方法的习惯,它返回Option类型,更安全。

// 安全的访问方式
if let Some(value) = arr.get(10) {
    println!("找到了: {}", value);
} else {
    println!("索引越界了");
}

4.3 类型推断与注解

Rust的编译器很聪明。它能根据上下文推断出变量的类型。但有时候,你也得明确告诉它。

let guess = "42".parse().expect("解析失败");
// 这行会报错,因为编译器不知道parse成什么类型

let guess: u32 = "42".parse().expect("解析失败");
// 加上类型注解,搞定

我个人习惯,在变量声明时尽量加上类型注解。虽然编译器能推断,但显式写出类型能让代码更清晰,也方便其他人阅读。

let count: u32 = 100;           // 清晰明了
let name: String = "Rust".into(); // 一眼看出类型

4.4 类型转换:用as关键字

Rust不会做隐式类型转换。你想把i32转成f64?得用as关键字显式转换。

let a: i32 = 10;
let b: f64 = a as f64;  // 10.0

let c: u8 = 255;
let d: i8 = c as i8;    // -1,注意溢出

踩坑提醒:我曾经在项目里用as把大整数转成小整数,结果数据被截断了。as转换是直接截断的,不会四舍五入。如果你需要安全的转换,用TryFrom trait。

// 安全的转换方式
use std::convert::TryFrom;

let big = 256u16;
let small = u8::try_from(big);
match small {
    Ok(v) => println!("转换成功: {}", v),
    Err(e) => println!("转换失败: {}", e),
}

浮点数转整数也是直接截断,不是四舍五入。

let f = 3.99;
let i = f as i32;  // 3,不是4

知识体系总览

下面这张图,把本章的核心内容串起来了。你可以看到数据类型的分支关系,以及它们之间的转换路径。

Rust 基础数据类型 标量类型 (Scalar) 整数 (i32, u8...) 浮点数 (f32, f64) 布尔 (bool) 字符 (char) 复合类型 (Compound) 元组 (Tuple) 数组 (Array) 类型推断与注解 类型转换 (as 关键字 / TryFrom) 标量类型是单个值,复合类型是多个值的组合 类型推断让代码简洁,类型注解让代码清晰 as 转换要小心溢出,推荐使用 TryFrom 做安全转换

我的建议:刚开始学Rust,别怕类型系统。多写、多试、多犯错。编译器会告诉你哪里不对。等你习惯了这种严谨,再回头看其他语言,你会觉得Rust的类型系统真香。


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