03. Basic Types — 基本类型
类型是 Rust 安全保证的基石。Rust 的类型系统在编译期执行严格的检查,杜绝了大多数语言中常见的类型错误和内存错误。本章系统介绍 Rust 的基本类型,理解它们的设计不仅有助于编写正确的代码,更有助于理解 Rust 所有权和并发模型的基础。
Rust 类型全景
下表列出了 Rust 标准库中的所有主要类型类别:
| 类别 | 类型 | 用途 | 大小 |
|---|---|---|---|
| 整数(有符号) | i8, i16, i32, i64, i128, isize | 有符号整数 | 8/16/32/64/128/指针宽度 |
| 整数(无符号) | u8, u16, u32, u64, u128, usize | 无符号整数 | 同上 |
| 浮点数 | f32, f64 | IEEE 浮点数 | 32/64 位 |
| 布尔 | bool | true / false | 1 字节 |
| 字符 | char | Unicode 标量值(码点) | 4 字节 |
| 元组 | (T1, T2, ...) | 异构定长组合 | 元素之和 |
| 数组 | [T; N] | 同构定长组合 | N * size_of::<T>() |
| 向量 | Vec<T> | 同构变长(堆分配) | 3 words(ptr + len + cap) |
| 切片 | &[T], &mut [T] | 数组/向量的引用区域 | 2 words(ptr + len) |
| 字符串切片 | &str, &mut str | UTF-8 文本的引用 | 2 words(ptr + len) |
| 自有字符串 | String | 可增长的 UTF-8 文本 | 3 words(ptr + len + cap) |
| 引用 | &T, &mut T | 不可变/可变借用 | 1 word(指针) |
| Box | Box<T> | 堆分配的单值 | 1 word(指针) |
| 裸指针 | *const T, *mut T | C 风格指针(unsafe) | 1 word |
| 函数指针 | fn(T1, T2) -> R | 函数指针 | 1 word |
| 闭包 | |a, b| { ... } | 匿名可调用值 | 可变(捕获环境) |
固定宽度整数类型
Rust 从 C 和现代 CPU 架构中借鉴了固定宽度整数类型,但做得更加严谨:
| 类型 | 范围 | 字节数 | 典型用途 |
|---|---|---|---|
u8 | 0 .. 2⁸−1 (0..255) | 1 | 字节处理、网络协议、b'X' |
u16 | 0 .. 2¹⁶−1 (0..65535) | 2 | 较窄的范围、老旧文件格式 |
u32 | 0 .. 2³²−1 (~42.9亿) | 4 | 颜色值、IPv4 地址 |
u64 | 0 .. 2⁶⁴−1 | 8 | 大计数器、时间戳 |
u128 | 0 .. 2¹²⁸−1 | 16 | 密码学、哈希 |
usize | 0 .. 2ᴺ−1 (N = CPU 位宽) | 4/8 | 数组索引、大小、计数 |
i8 | −128 .. 127 | 1 | 小有符号值 |
i16 | −32768 .. 32767 | 2 | 音频采样 |
i32 | −2³¹ .. 2³¹−1 | 4 | 默认整数类型 |
i64 | −2⁶³ .. 2⁶³−1 | 8 | 大范围值、Unix 时间戳 |
i128 | −2¹²⁷ .. 2¹²⁷−1 | 16 | 极精确的固定点计算 |
isize | −2ᴺ⁻¹ .. 2ᴺ⁻¹−1 | 4/8 | 有符号索引、指针差 |
为什么 Rust 的默认整数是 i32? 在现代 32/64 位 CPU 上,32 位整数的运算速度最快,且范围足够大多数场景(−21 亿到 21 亿)。i32 而不是 u32 被选为默认,因为无符号整数在与有符号值互动时会产生令人困惑的隐式转换——这在 C/C++ 中是 bug 的常见来源。
整数字面量
Rust 的整数字面量非常灵活:
let a = 42; // 十进制,推断为 i32
let b = 0x2A; // 十六进制
let c = 0o52; // 八进制
let d = 0b0010_1010; // 二进制,_ 增强可读性
let e = b'*'; // 字节字面量(u8),值为 42
// 后缀可以指定具体类型
let f = 42u8; // u8
let g = 1_000_000i64; // i64
let h = 0xffu32; // u32
let i = b'*' as u64; // u64,通过 as 转换
assert_eq!(a, 42);
assert_eq!(e, 42u8); // b'*' 的 ASCII 值是 42下划线 _ 可以出现在数字字面量的任意位置,编译器直接忽略它们。这对于大数字的可读性至关重要:
let million = 1_000_000;
let binary = 0b1111_0000_1010_0101;
let hex = 0xDEAD_BEEF_CAFE_BABE_u64;字节字面量 b'X' 表示字符 X 的 ASCII 码值(类型 u8)。非 ASCII 字符不允许用于字节字面量:
let a = b'A'; // 65u8
let b = b'\n'; // 10u8
// let c = b'中'; // 编译错误:非 ASCII 字符不能用于字节字面量整数溢出
Rust 在调试模式(cargo build,默认用 debug profile)下检测整数溢出并 panic。在发布模式(cargo build --release)下则回绕(wraps around)——这是为了性能,与 C/C++ 的未定义行为不同。
你可以显式选择溢出行为:
let a: u8 = 200;
let b: u8 = 100;
// 1. checked_* —— 返回 Option,溢出为 None
assert_eq!(a.checked_add(b), None); // 200 + 100 > 255
assert_eq!(10_u8.checked_add(20), Some(30)); // 正常情况
// 2. wrapping_* —— 回绕(模运算),release profile 默认行为
assert_eq!(a.wrapping_add(b), 44); // (200 + 100) % 256 = 44
assert_eq!(0u8.wrapping_sub(1), 255); // 255
// 3. saturating_* —— 饱和,在边界停止
assert_eq!(a.saturating_add(b), 255); // max
assert_eq!(0u8.saturating_sub(1), 0); // min
// 4. overflowing_* —— 返回 (result, bool),bool 指示是否溢出
assert_eq!(a.overflowing_add(b), (44, true)); // 溢出
assert_eq!(10_u8.overflowing_add(20), (30, false)); // 未溢出工程建议:对于性能和正确性都重要的场景,使用 wrapping_* 配合注释。对于正确性优先的场景,使用 checked_*。对于传感器读数或信号值,使用 saturating_*。
浮点类型
Rust 的浮点数遵循 IEEE 754-2008 标准:
| 类型 | 精度 | 值域 | 大小 |
|---|---|---|---|
f32 | 约 7 位有效十进制数 | ≈ 1.4e-45 .. 3.4e+38 | 4 字节 |
f64 | 约 15 位有效十进制数 | ≈ 5.0e-324 .. 1.8e+308 | 8 字节 |
默认浮点类型是 f64,因为在现代 CPU 上 f64 的速度与 f32 几乎相同,但精度更高、范围更广。
let pi: f64 = 3.14159265358979;
let e = 2.718281828459045; // 类型推断为 f64
// 特殊值
assert!(f64::NAN.is_nan()); // 非数字
assert_eq!(f64::INFINITY, f64::INFINITY); // 无穷大
assert_eq!(f64::NEG_INFINITY, f64::NEG_INFINITY);
// 与 C/C++ 不同,Rust 的 NaN 不等于自身(符合 IEEE)
assert_ne!(f64::NAN, f64::NAN);
// 浮点字面量可以加下划线
let avogadro = 6.022_140_76e23_f64;浮点类型实现了 Copy,这意味着赋值时会按位复制,不会发生所有权转移:
let x = 3.14;
let y = x; // x 被复制,x 仍可用
println!("{}", x); // OK布尔类型
Rust 的 bool 极其严格:
let is_ok: bool = true;
let is_err = false;
// 可以作为 if 条件
if is_ok {
println!("Everything is fine.");
}
// 可以用 as 转换为整数
assert_eq!(true as i32, 1);
assert_eq!(false as i32, 0);
// 但不能隐式转换——与 C/C++ 不同
// if 1 { } // 编译错误:期待 bool,找到整数这个严格性是有意设计的。在 C 中,if (x = 5) 是一个臭名昭著的 bug 来源——它是赋值而非比较,但 5 被隐式转换为 true。Rust 通过要求条件必须是 bool 类型来避免这类错误。
字符类型
Rust 的 char 是 32 位 Unicode 标量值(scalar value),不是 ASCII 字节。与 C 的 char(8 位)、C++ 的 char(8 位)和 Java 的 char(16 位 UTF-16 代码单元)都不同:
let caps_a: char = 'A';
let lowercase_a = 'a';
let kanji = '漢'; // CJK 统一表意文字
let emoji = '😻'; // 表情符号(U+1F63B)
let math_z = 'ℤ'; // 数学符号
// char 占 4 字节
use std::mem::size_of;
assert_eq!(size_of::<char>(), 4);
// 字符的 Unicode 标量值
assert_eq!('A' as u32, 65);
assert_eq!('😻' as u32, 0x1F63B);
// unicode 转义序列
assert_eq!('\u{41}', 'A');
assert_eq!('\u{1F63B}', '😻');
// 从 u32 构造 char(可能失败)
assert_eq!(char::from_u32(65), Some('A'));
assert_eq!(char::from_u32(0x110000), None); // 超出 Unicode 范围
// 其他转义
assert_eq!('\n', 0x0A as char); // 换行
assert_eq!('\t', 0x09 as char); // 制表符
assert_eq!('\\', '\\'); // 反斜杠
assert_eq!('\'', '\''); // 单引号
assert_eq!('\"', '"'); // 双引号(在 char 中可以省略反斜杠)
assert_eq!('\x41', 'A'); // 十六进制(<= 0x7F)为什么 char 是 32 位? Rust 的设计决定「字符串是 UTF-8 编码的字节序列([u8]),但单个字符是完整的 Unicode 标量值」。这使得字符操作在语义上干净——你不会像 JavaScript 或 Java 那样遇到代理对(surrogate pair)的麻烦。
元组
元组是异构的、固定长度的序列:
// 创建
let t: (i32, f64, char) = (42, 3.14, 'π');
// 索引访问(使用 .0 .1 .2 ...)
assert_eq!(t.0, 42);
assert_eq!(t.1, 3.14);
assert_eq!(t.2, 'π');
// 解构
let (x, y, z) = t;
assert_eq!(x, 42);
assert_eq!(y, 3.14);
// 函数可以通过元组返回多个值
fn split_at(s: &str, mid: usize) -> (&str, &str) {
(&s[..mid], &s[mid..])
}
let (left, right) = split_at("hello world", 5);
assert_eq!(left, "hello");
assert_eq!(right, " world");
// 单元类型(unit type)
let unit: () = ();
assert_eq!(size_of::<()>(), 0);单元类型 () 在 Rust 中普遍存在:没有返回值的函数实际返回 ();Result<T, ()> 用于不关心错误详情的场景;Vec<()> 是表示固定计数的一种方式。
指针类型
Rust 的指针形成了明确的层级结构:
| 类型 | 含义 | 适用场景 |
|---|---|---|
&T | 不可变引用(共享引用) | safe Rust,读数据 |
&mut T | 可变引用(独占引用) | safe Rust,修改数据 |
Box<T> | 堆分配、拥有所有权的指针 | 递归类型、大数据、Trait 对象 |
*const T | 不可变裸指针 | unsafe 块,FFI |
*mut T | 可变裸指针 | unsafe 块,FFI |
// 引用
let x = 42;
let r: &i32 = &x; // 不可变引用
assert_eq!(*r, 42); // 解引用
let mut y = 42;
let rm: &mut i32 = &mut y; // 可变引用
*rm += 1; // 解引用后赋值
assert_eq!(y, 43);
// Box:堆分配
let b = Box::new([0u8; 1024]); // 1KB 在堆上,指针在栈上
assert_eq!(b.len(), 1024);
// 裸指针(仅在 unsafe 块中解引用)
let p: *const i32 = &x;
unsafe {
println!("{}", *p); // 允许,但编译器不保证安全
}数组、向量与切片
这三种类型展示了 Rust 从「编译期已知」到「运行时可变」的平滑过渡:
数组 [T; N]
// 定长数组在栈上分配
let a: [i32; 3] = [1, 2, 3];
let zeros: [u8; 1024] = [0; 1024]; // 1024 个零
assert_eq!(a.len(), 3);
assert_eq!(a[0], 1);
// 遍历
for x in &a {
println!("{}", x);
}
// 编译期保证:长度是类型的一部分
// fn takes_three(arr: [i32; 3]) { ... }
// takes_three([1, 2]); // 错误:预期长度为 3,实际长度为 2
// takes_three([1, 2, 3, 4]); // 错误:预期长度为 3,实际长度为 4向量 Vec<T>
// 可变长度的堆分配数组
let mut v: Vec<i32> = Vec::new();
v.push(1);
v.push(2);
v.push(3);
// vec! 宏
let v2 = vec![1, 2, 3];
let v3 = vec![0; 100]; // 100 个零
// 容量管理
assert!(v.capacity() >= v.len()); // 容量 >= 长度
v.reserve(100); // 预留空间
// 访问
assert_eq!(v[0], 1);
match v.get(10) {
Some(x) => println!("{}", x),
None => println!("Index out of bounds"),
}
// 无法从 Vec 中直接移出元素(需要非索引方式)
// let x = v[0]; // 编译错误:cannot move out of index切片 &[T] 和 &mut [T]
切片是对数组或向量的一个连续区域的胖指针(fat pointer),包含指向第一个元素的指针和元素数量:
let v = vec![1, 2, 3, 4, 5];
// 切片:胖指针(ptr, len)——2 words
let slice: &[i32] = &v[1..4]; // [2, 3, 4]
assert_eq!(slice.len(), 3);
assert_eq!(slice[0], 2);
// 切片可以进一步切片
let sub_slice: &[i32] = &slice[..2]; // [2, 3]
// 可变切片
let mut v = vec![1, 2, 3, 4, 5];
let mut_slice: &mut [i32] = &mut v[..3];
mut_slice[0] = 10;
assert_eq!(v, vec![10, 2, 3, 4, 5]);
// 排序
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
v.sort(); // v.sort() 等价于 (&mut v[..]).sort()
assert_eq!(v, vec![1, 1, 2, 3, 4, 5, 6, 9]);内存布局对比:
数组 [i32; 5]: |1|2|3|4|5| (栈上,连续)
向量 Vec<i32>: |ptr|len|cap| -> [1,2,3,4,5,...] (栈上 3 words + 堆上数据)
切片 &[i32]: |ptr|len| -> 指向某个 [i32; N] 或 Vec<i32> 的数据字符串类型
Rust 的字符串模型可能是初学者最困惑的部分之一,但它的设计逻辑非常清晰:
// &str:字符串切片——对 UTF-8 数据的不可变引用(胖指针)
let cooking: &str = "Souffle";
let chinese: &str = "你好世界";
// 字节长度 ≠ 字符数量(UTF-8 编码中,一个字符可能占 1-4 字节)
assert_eq!(cooking.len(), 7); // 全部 ASCII,7 字节 = 7 字符
assert_eq!(chinese.len(), 12); // 4 个中文字符,每字 3 字节(UTF-8)= 12 字节
assert_eq!(chinese.chars().count(), 4); // 字符数 = 4
// String:在堆上可增长的 UTF-8 字符串
let mut s = String::from("Hello");
s.push_str(", world!");
s.push('!');
assert_eq!(s, "Hello, world!!");
// &str 和 String 的互通
let s = String::from("hello");
let slice: &str = &s; // String -> &str(自动解引用,因为 String: Deref<Target=str>)
let s2 = slice.to_string(); // &str -> String
let s3 = slice.to_owned(); // &str -> String(等价于 to_string())字符串字面量的更多形式
// 原始字符串(raw string):不需要转义
let raw = r#"{"name": "Alice", "age": 30}"#; // JSON
let path = r"C:\Users\Alice\Documents"; // Windows 路径
// 带定界符的原始字符串
let long = r###"
这里不需要转义任何内容。
引号 " 也能随意使用。
井号 # 也没问题——只要不成对出现 ###。
"###;
// 字节字符串(&[u8],不是 &str)
let bytes: &[u8; 5] = b"hello";
assert_eq!(bytes, &[104, 101, 108, 108, 111]);
// 原始字节字符串
let raw_bytes = br#"{"key": "value"}"#;&str vs String 的对比:
&str | String | |
|---|---|---|
| 存储位置 | 栈上胖指针(ptr + len),数据可在栈/堆/静态区 | 栈上 3 words(ptr + len + cap),数据在堆上 |
| 可变 | 不可变 | 可增长、可修改 |
| 实现 Trait | Copy | Clone(深拷贝堆数据) |
| 获取方式 | 字面量、切片、从 String 借出 | String::from()、to_string()、format!() |
| 内存释放 | 跟随其所引用的数据 | 离开作用域时自动释放堆数据 |
类型别名
使用 type 关键字为已有类型创建别名(不是新类型):
type Count = usize;
type Point = (f64, f64);
type UserId = u64;
fn get_user(id: UserId) -> Option<String> {
// ...
None
}
let count: Count = 42;
assert_eq!(count, 42usize); // Count 就是 usize注意:类型别名只是同义词,不创建新的类型安全性。如果需要区分 UserId 和 u64,应该使用新类型模式(newtype pattern):
struct UserId(u64); // 包装成新类型
struct GroupId(u64); // 不同于 UserId
fn get_user(id: UserId) { /* ... */ }
// get_user(42); // 错误:预期 UserId,找到整数
// get_user(GroupId(42)); // 错误:预期 UserId,找到 GroupId小结
- Rust 的整数类型是固定宽度的,不随平台变化(
usize/isize除外)。默认整数i32,默认浮点f64。 - 整数溢出在调试模式 panic,发布模式回绕。四种显式方法(
checked_*、wrapping_*、saturating_*、overflowing_*)允许精确控制溢出行为。 bool极其严格,不接受整数隐式转换;char是 32 位 Unicode 标量值,不同于 C/Java。- 数组、向量、切片形成从编译期固定到运行时可变的三层结构:
[T; N](栈上定长) ->Vec<T>(堆上变长) ->&[T](引用任意连续区域)。 &str和String是 Rust 字符串模型的两面:&str是借用、零拷贝、不可变;String是拥有所有权、可变、可增长。它们通过Deref无缝互通。- 指针类型分为三层:安全引用(
&T/&mut T)、堆所有(Box<T>)、unsafe 裸指针(*const T/*mut T)。 - 类型别名只是同义词,需要类型安全时使用 newtype 模式。