数据类型
问题
Rust 有哪些数据类型?类型系统有什么特点?
答案
Rust 是静态强类型语言——每个值在编译期都必须有确定的类型,且不支持隐式类型转换。同时 Rust 有强大的类型推断能力,大部分场景不需要手动标注类型。
标量类型(Scalar Types)
标量类型代表单个值,有四大类:
整数
| 长度 | 有符号 | 无符号 | 范围(有符号) |
|---|---|---|---|
| 8-bit | i8 | u8 | ~ |
| 16-bit | i16 | u16 | ~ |
| 32-bit | i32(默认) | u32 | ~ |
| 64-bit | i64 | u64 | ~ |
| 128-bit | i128 | u128 | ~ |
| 平台相关 | isize | usize | 取决于平台(32/64 位) |
fn main() {
let decimal = 98_222; // 十进制(可用 _ 分隔增强可读性)
let hex = 0xff; // 十六进制
let octal = 0o77; // 八进制
let binary = 0b1111_0000; // 二进制
let byte = b'A'; // 字节(u8)
let x: i32 = 42; // 默认整数类型是 i32
let index: usize = 0; // 数组索引必须是 usize
}
整数溢出
- Debug 模式:整数溢出会 panic
- Release 模式:整数溢出执行二进制补码包装(wrapping),不会 panic
如果需要显式控制溢出行为,使用标准库方法:
let x: u8 = 255;
x.wrapping_add(1); // 0(包装)
x.checked_add(1); // None(检查溢出)
x.saturating_add(1); // 255(饱和)
x.overflowing_add(1); // (0, true)(返回溢出标志)
浮点数
let x = 2.0; // f64(默认)
let y: f32 = 3.0; // f32
// 浮点数遵循 IEEE 754 标准
assert!(0.1 + 0.2 != 0.3); // ⚠️ 浮点精度问题
assert!((0.1_f64 + 0.2 - 0.3).abs() < f64::EPSILON); // ✅ 正确比较方式
布尔与字符
let t: bool = true;
let f = false;
// char 是 4 字节 Unicode 标量值,不是 1 字节
let c: char = 'z';
let emoji: char = '🦀';
let chinese: char = '中';
assert_eq!(std::mem::size_of::<char>(), 4); // char 占 4 字节
复合类型(Compound Types)
元组(Tuple)
固定长度,各元素可以是不同类型:
fn main() {
let tup: (i32, f64, bool) = (500, 6.4, true);
// 解构
let (x, y, z) = tup;
// 索引访问(从 0 开始)
let first = tup.0; // 500
let second = tup.1; // 6.4
// 单元元组 ()(unit type)——空元组,不占内存
let unit: () = ();
}
单元类型
()() 是 Rust 的"空值",等价于其他语言的 void。不返回值的函数实际返回 ()。
fn do_nothing() {
// 等价于 fn do_nothing() -> () { () }
}
数组(Array)
固定长度,所有元素同一类型,栈上分配:
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10]; // 创建 10 个元素都是 0 的数组
let first = arr[0];
let len = arr.len(); // 5
// 数组越界会在运行时 panic(编译期可能检查不到)
// let out = arr[10]; // ⚠️ index out of bounds: panic
}
切片(Slice)
对连续序列的引用视图,没有所有权:
fn main() {
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // [2, 3, 4]
let full: &[i32] = &arr[..]; // 全部
let from: &[i32] = &arr[2..]; // [3, 4, 5]
let to: &[i32] = &arr[..3]; // [1, 2, 3]
}
类型转换
Rust 不支持隐式类型转换,必须显式:
fn main() {
// as 关键字:基本类型之间的转换
let x: i32 = 42;
let y: f64 = x as f64;
let z: u8 = x as u8; // 截断高位
// ⚠️ as 转换可能丢失精度或截断
let big: i32 = 300;
let small: u8 = big as u8; // 300 % 256 = 44
// From/Into trait:安全的类型转换
let s = String::from("hello"); // From
let num: i64 = 42_i32.into(); // Into
// TryFrom/TryInto:可能失败的转换
let big_num: i32 = 1_000_000;
let result: Result<u16, _> = big_num.try_into(); // Err(超出 u16 范围)
}
| 方法 | 安全性 | 说明 |
|---|---|---|
as | 可能丢失精度 | 基本类型之间的转换 |
From / Into | 安全、不会失败 | 保证无损转换 |
TryFrom / TryInto | 返回 Result | 可能失败的转换 |
更多类型转换内容请参阅 类型转换
类型推断
Rust 编译器能根据上下文自动推断类型:
fn main() {
let x = 5; // 推断为 i32
let y = 5.0; // 推断为 f64
let v = vec![1, 2, 3]; // 推断为 Vec<i32>
// 有时需要类型标注帮助编译器
let parsed: i64 = "42".parse().unwrap(); // parse 需要知道目标类型
// 或者用 turbofish 语法
let parsed = "42".parse::<i64>().unwrap();
}
类型别名
type Kilometers = i32;
type Result<T> = std::result::Result<T, std::io::Error>;
// 类型别名不创建新类型,只是别名(可以和原类型混用)
let distance: Kilometers = 5;
let x: i32 = distance + 10; // ✅ Kilometers 就是 i32
如果想创建语义不同的新类型(阻止混用),使用 Newtype 模式:
struct Meters(f64);
struct Seconds(f64);
// Meters 和 Seconds 不能直接相加,即使底层都是 f64
更多内容请参阅 Newtype 模式
永远不返回的类型 !
! 是 never type,表示函数永远不会返回:
fn diverges() -> ! {
panic!("this function never returns");
}
// 常见于:
// - panic!()
// - loop {}(无限循环)
// - std::process::exit()
// - match 中的 unreachable!()
! 可以被强制转换为任意类型,这也是为什么 panic!() 可以出现在任意分支中:
let x: i32 = match some_option {
Some(v) => v,
None => panic!("no value"), // panic! 返回 !,可以当 i32 用
};
常见面试问题
Q1: Rust 的 i32 和 isize 有什么区别?什么时候用哪个?
答案:
i32:固定 32 位有符号整数,与平台无关isize:平台相关的有符号整数,32 位系统上是 32 位,64 位系统上是 64 位
使用场景:
- 数组索引必须使用
usize - 一般计算默认用
i32(编译器默认类型) - 与指针运算相关的场景用
isize/usize - FFI时匹配 C 的
size_t(对应usize)
Q2: Rust 的 char 和其他语言有什么不同?
答案:
Rust 的 char 是 4 字节的 Unicode 标量值(U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF),而不是其他语言中常见的 1 字节 ASCII 字符。它可以表示任何 Unicode 字符,包括中文、emoji 等。
assert_eq!(std::mem::size_of::<char>(), 4);
let c = '🦀'; // 合法的 char
注意 char 和字符串中的字符不同——字符串是 UTF-8 编码,一个中文字符在 String 中占 3 字节,而 char 固定 4 字节。
Q3: 数组和 Vec 有什么区别?
答案:
| 特性 | 数组 [T; N] | Vec<T> |
|---|---|---|
| 大小 | 编译期固定 | 运行时可变 |
| 存储 | 栈上 | 堆上 |
| 性能 | 更快(无间接寻址) | 有堆分配开销 |
| 使用场景 | 大小已知的小数据 | 动态集合 |
let arr: [i32; 3] = [1, 2, 3]; // 栈上,大小固定
let vec: Vec<i32> = vec![1, 2, 3]; // 堆上,可 push
更多集合内容请参阅 常用集合
Q4: 什么是单元类型 ()?有什么作用?
答案:
() 是空元组,称为单元类型(unit type),大小为 0。它的作用类似其他语言的 void:
- 不返回有意义值的函数隐式返回
() HashMap<K, ()>可以当 HashSet 用Result<(), Error>表示操作成功但无返回值- 语句的值是
()(如let x = 5;返回())
Q5: as 转换和 From/Into 有什么区别?应该用哪个?
答案:
| 方面 | as | From/Into |
|---|---|---|
| 安全性 | 可能截断、溢出 | 编译器保证无损 |
| 适用范围 | 基本类型、裸指针 | 任何实现了 trait 的类型 |
| 可扩展性 | 内置操作,不可自定义 | 可以为自定义类型实现 |
| 推荐度 | 仅在明确知道行为时使用 | 优先推荐 |
最佳实践:
- 优先用
From/Into(安全、可读) - 可能失败时用
TryFrom/TryInto - 只在需要截断或原始类型转换时用
as
Q6: 什么是 never type !?有什么实际用途?
答案:
! 表示函数永远不会正常返回(发散函数)。它可以被强制转换为任何类型,这在类型系统中有重要作用:
// 1. match 分支类型统一
let value: i32 = match input.parse::<i32>() {
Ok(n) => n, // i32
Err(_) => panic!(), // !,可作为 i32
};
// 2. loop 的返回类型
let result: String = loop {
break String::from("done"); // 用 break 返回值
};
// 3. 标记不可能的分支
enum Void {} // 没有变体,不可能被实例化