Skip to content
Published at:

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, f64IEEE 浮点数32/64 位
布尔booltrue / false1 字节
字符charUnicode 标量值(码点)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 strUTF-8 文本的引用2 words(ptr + len)
自有字符串String可增长的 UTF-8 文本3 words(ptr + len + cap)
引用&T, &mut T不可变/可变借用1 word(指针)
BoxBox<T>堆分配的单值1 word(指针)
裸指针*const T, *mut TC 风格指针(unsafe)1 word
函数指针fn(T1, T2) -> R函数指针1 word
闭包|a, b| { ... }匿名可调用值可变(捕获环境)

固定宽度整数类型

Rust 从 C 和现代 CPU 架构中借鉴了固定宽度整数类型,但做得更加严谨:

类型范围字节数典型用途
u80 .. 2⁸−1 (0..255)1字节处理、网络协议、b'X'
u160 .. 2¹⁶−1 (0..65535)2较窄的范围、老旧文件格式
u320 .. 2³²−1 (~42.9亿)4颜色值、IPv4 地址
u640 .. 2⁶⁴−18大计数器、时间戳
u1280 .. 2¹²⁸−116密码学、哈希
usize0 .. 2ᴺ−1 (N = CPU 位宽)4/8数组索引、大小、计数
i8−128 .. 1271小有符号值
i16−32768 .. 327672音频采样
i32−2³¹ .. 2³¹−14默认整数类型
i64−2⁶³ .. 2⁶³−18大范围值、Unix 时间戳
i128−2¹²⁷ .. 2¹²⁷−116极精确的固定点计算
isize−2ᴺ⁻¹ .. 2ᴺ⁻¹−14/8有符号索引、指针差

为什么 Rust 的默认整数是 i32 在现代 32/64 位 CPU 上,32 位整数的运算速度最快,且范围足够大多数场景(−21 亿到 21 亿)。i32 而不是 u32 被选为默认,因为无符号整数在与有符号值互动时会产生令人困惑的隐式转换——这在 C/C++ 中是 bug 的常见来源。

整数字面量

Rust 的整数字面量非常灵活:

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

下划线 _ 可以出现在数字字面量的任意位置,编译器直接忽略它们。这对于大数字的可读性至关重要:

rust
let million = 1_000_000;
let binary = 0b1111_0000_1010_0101;
let hex = 0xDEAD_BEEF_CAFE_BABE_u64;

字节字面量 b'X' 表示字符 X 的 ASCII 码值(类型 u8)。非 ASCII 字符不允许用于字节字面量:

rust
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++ 的未定义行为不同。

你可以显式选择溢出行为:

rust
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+384 字节
f64约 15 位有效十进制数≈ 5.0e-324 .. 1.8e+3088 字节

默认浮点类型是 f64,因为在现代 CPU 上 f64 的速度与 f32 几乎相同,但精度更高、范围更广。

rust
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,这意味着赋值时会按位复制,不会发生所有权转移:

rust
let x = 3.14;
let y = x;      // x 被复制,x 仍可用
println!("{}", x);  // OK

布尔类型

Rust 的 bool 极其严格:

rust
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 的 char32 位 Unicode 标量值(scalar value),不是 ASCII 字节。与 C 的 char(8 位)、C++ 的 char(8 位)和 Java 的 char(16 位 UTF-16 代码单元)都不同:

rust
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)的麻烦。

元组

元组是异构的、固定长度的序列:

rust
// 创建
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
rust
// 引用
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]

rust
// 定长数组在栈上分配
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>

rust
// 可变长度的堆分配数组
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),包含指向第一个元素的指针和元素数量:

rust
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 的字符串模型可能是初学者最困惑的部分之一,但它的设计逻辑非常清晰:

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())

字符串字面量的更多形式

rust
// 原始字符串(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 的对比:

&strString
存储位置栈上胖指针(ptr + len),数据可在栈/堆/静态区栈上 3 words(ptr + len + cap),数据在堆上
可变不可变可增长、可修改
实现 TraitCopyClone(深拷贝堆数据)
获取方式字面量、切片、从 String 借出String::from()to_string()format!()
内存释放跟随其所引用的数据离开作用域时自动释放堆数据

类型别名

使用 type 关键字为已有类型创建别名(不是新类型):

rust
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

注意:类型别名只是同义词,不创建新的类型安全性。如果需要区分 UserIdu64,应该使用新类型模式(newtype pattern)

rust
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](引用任意连续区域)。
  • &strString 是 Rust 字符串模型的两面:&str 是借用、零拷贝、不可变;String 是拥有所有权、可变、可增长。它们通过 Deref 无缝互通。
  • 指针类型分为三层:安全引用(&T/&mut T)、堆所有(Box<T>)、unsafe 裸指针(*const T/*mut T)。
  • 类型别名只是同义词,需要类型安全时使用 newtype 模式。