跳转到内容

A.1 标量、元组与单元类型

本文集中梳理主线最常出现的几类内置基础类型。它们有一个共同特征:编译期大小全部已知,直接存放在栈上,且编译期类型实体全部实现了 Copy(赋值与传参时直接按位复制,原变量不会失效)。

一、标量类型:整数、usize、浮点、布尔与 char

Section titled “一、标量类型:整数、usize、浮点、布尔与 char”

先运行下面这段短程序,实测几个核心标量在栈上占用的字节数:

源文件:scalars.rs
use std::mem::size_of;
fn main() {
let count: usize = 3;
let flag: bool = true;
let letter: char = '中';
println!("{count} {flag} {letter}");
println!(
"i32={} char={} bool={}",
size_of::<i32>(),
size_of::<char>(),
size_of::<bool>()
);
}
在 Rust Playground 中运行

程序输出:

3 true 中
i32=4 char=4 bool=1

1. 整数家族与为什么索引一律用 usize

Section titled “1. 整数家族与为什么索引一律用 usize”

Rust 的整数按「有符号(i 开头,可表示正负数)」与「无符号(u 开头,仅表示非负数)」以及位宽精确命名:

位宽(占字节数) 有符号整数 无符号整数 说明
8 位(1 字节) i8 u8 u8 常用于表示原始字节数据(如网络包、UTF-8 字节流)
16 位(2 字节) i16 u16 固定宽度短整数
32 位(4 字节) i32 u32 未写类型标注时,整数字面量默认推断为 i32
64 位(8 字节) i64 u64 固定宽度长整数
128 位(16 字节) i128 u128 超大整数计算
随平台字长 isize usize 在 64 位机器上固定占 8 字节(在 32 位机器上占 4 字节)

为什么 Rust 的数组长度、集合容量和下标索引(arr[i])强制要求使用 usize,而不能直接用 i32?

因为索引和长度在底层对应内存地址的偏移量,既不能是负数,也必须能覆盖当前机器的整个内存寻址空间。在 64 位机器上,usize 固定为 8 字节无符号整数,刚好与一个内存地址(机器字)等宽。Rust 不允许不同位宽的整数隐式混用,如需转换须显式写 x as usize。

(注:整数字面量可用下划线辅助阅读,如 1_000_000;也支持直接在字面量末尾写类型后缀,如 42u8、3usize。)

  • 浮点数:只有 f32(4 字节单精度)和 f64(8 字节双精度,浮点字面量默认推断为 f64)。
  • 布尔值(bool):只有 true 和 false 两个值,在内存中固定占 1 个字节。Rust 的 if 条件位置只接受 bool,不会像 C 或 Python 那样把整数 0 或空指针自动当成 false。

3. 字符类型 char:为什么占 4 个字节?

Section titled “3. 字符类型 char:为什么占 4 个字节?”

在 C 语言中,char 只占 1 个字节(ASCII);在 Java 中,char 占 2 个字节(UTF-16 代码单元)。

而在 Rust 中,单引号写出的字符字面量(如 'a'、'中'、'🦀')类型为 char,在栈上固定占 4 个字节。因为它代表的是一个完整的 Unicode 标量值(Unicode Scalar Value),能直接装下世界上任何语言的文字或 Emoji。

(注意区分单引号与双引号:单引号 'a' 是占 4 字节的单个字符 char;双引号 "a" 则是 UTF-8 编码的字符串字面量,在第 4 章字符串一节中会详细拆解。)


二、元组(Tuple)与单元类型 ()(Unit Type)

Section titled “二、元组(Tuple)与单元类型 ()(Unit Type)”

当你想把几个不同类型的值临时打包在一起(比如让函数一次返回两个值),又不想专门定义一个结构体时,可以使用元组。

源文件:tuples-and-unit.rs
use std::mem::size_of;
fn log_message(msg: &str) {
println!("{msg}");
}
fn main() {
let pair: (i32, bool) = (42, true);
let (num, ok) = pair;
let first = pair.0;
let result: () = log_message("ready");
println!("{num} {ok} {first} {:?} size_of_()={}", result, size_of::<()>());
}
在 Rust Playground 中运行

程序输出:

42 true 42 () size_of_()=0

1. 元组的创建、解构与按下标访问

Section titled “1. 元组的创建、解构与按下标访问”
  • 类型与长度编译期固定:let pair: (i32, bool) = (42, true); 把一个 4 字节整数和一个 1 字节布尔值打包成一个复合类型。一旦声明,元组的长度和每个位置上的类型都固定不变。
  • 两种取出元素的方式:
    1. 模式解构:let (num, ok) = pair; 一次性把里面的元素分别绑定到两个新变量上;
    2. 点号加数字下标:pair.0 取出第一个元素(42),pair.1 取出第二个元素(true)。
  • 复制与移动规则:当元组里的每一个元素类型都实现了 Copy(如 (i32, bool))时,整个元组也自动实现 Copy;只要其中包含未实现 Copy 的元素(如 (String, i32)),整个元组赋值时就会触发移动(Move)。

如果一个元组里一个元素都不装,写出来就是一对空括号 ()。它在 Rust 里有一个专门的名字,叫作单元类型(Unit Type),它唯一的值也写作 ()。

看清 () 的两个关键事实,能帮你读懂大量编译器报错:

  1. 用来取代其他语言里的 void:在 C 或 Java 中,不返回任何有意义结果的函数标为 void(不是一个真正的类型)。而 Rust 没有 void——像上面的 fn log_message(msg: &str) 这样没有写 -> ReturnType 的函数,实际上全部默认返回单元类型 -> ()。
  2. 在内存中占 0 个字节(零大小类型 ZST):正如上面代码中 size_of::<()>() 实测为 0,单元类型纯粹是编译期类型实体层面的标记。到了运行时,它不占用哪怕 1 个字节的栈空间,也不会生成任何搬运数据的机器指令。

当你看到编译器报错提示 expected i32, found () 时,意思是「编译器预期这里产出一个 i32 整数,但你的代码实际交出的是空值 ()」——这通常是因为你在函数最后一行不小心多加了一个分号。下一节马上拆解这个分号规则。