标量类型
Rust 是静态类型语言——每个值在编译时就有确定的类型。不用担心,Rust 的类型推断能力很强,大多数时候你不需要手动写类型,但理解它们是写出正确代码的基础。
整数类型
整数是最常用的类型。Rust 把整数分为有符号(可以是负数)和无符号(只能是非负数)两大类,并按位宽细分:
| 位宽 | 有符号 | 无符号 | 范围(有符号) |
|---|---|---|---|
| 8 位 | i8 | u8 | -128 ~ 127 |
| 16 位 | i16 | u16 | -32768 ~ 32767 |
| 32 位 | i32 | u32 | -约21亿 ~ 约21亿 |
| 64 位 | i64 | u64 | 极大范围 |
| 128 位 | i128 | u128 | 更大 |
| 指针宽度 | isize | usize | 取决于 CPU 架构(通常为32位或64位) |
fn main() {
let a: i32 = -42; // 有符号,可以是负数
let b: u32 = 100; // 无符号,只能非负
let c: u8 = 255; // u8 最大值
let d: i64 = 9_999_999_999; // 大数用下划线分隔,更易读
println!("a={} b={} c={} d={}", a, b, c, d);
}不写类型时的默认值:整数默认推断为 i32,这是最常用的整数类型。isize 和 usize 的宽度取决于 CPU 架构(64 位系统上是 64 位),主要用于集合的索引,如 arr[i] 中的 i 就是 usize。
有符号 vs 无符号怎么选? 当值永远不会为负时用无符号(如数组长度
usize、字节数据u8);其他情况用有符号更安全。
整型溢出:
u8最大值是 255,赋值 256 会怎样?在 Debug 模式(cargo build)下,Rust 会 panic——程序崩溃并报错,帮你发现 bug。在 Release 模式(cargo build --release)下,Rust 不 panic,而是做”二进制补码包裹”:256 变 0,257 变 1……程序不崩溃,但值是错的。如果你需要有意地处理溢出,可以用wrapping_add、saturating_add、checked_add等方法。
浮点数
Rust 有两种浮点类型:
| 类型 | 精度 | 说明 |
|---|---|---|
f32 | 单精度(约 7 位有效数字) | 性能更好,精度较低 |
f64 | 双精度(约 15 位有效数字) | 默认类型,精度更高 |
fn main() {
let x = 3.14; // 默认 f64
let y: f32 = 2.0; // 显式指定 f32
let z = 1.0_f64; // 用后缀指定类型
println!("{} {} {}", x, y, z);
// 浮点运算
println!("圆面积 = {:.4}", 3.14159 * 2.0_f64 * 2.0_f64);
}浮点数有精度误差,不要用
==直接比较两个浮点数是否相等,应使用差值是否足够小来判断。
布尔型与字符型
布尔型 bool 只有两个值:true 和 false,常用于条件判断。
fn main() {
let is_active: bool = true;
let is_empty = false; // 类型推断
println!("active: {}, empty: {}", is_active, is_empty);
println!("AND: {}, OR: {}, NOT: {}",
is_active && is_empty,
is_active || is_empty,
!is_active);
}字符型 char 表示单个 Unicode 字符,用单引号包裹,占 4 字节:
fn main() {
let c1 = 'A';
let c2 = '中'; // 汉字也是合法的 char
let c3 = '😀'; // 表情符号同样可以
let c4 = '\n'; // 转义字符
println!("{} {} {} (换行在这里{})", c1, c2, c3, c4);
}
char是 4 字节的 Unicode 标量值,而不是 ASCII 字节。这和 C 语言的char(1 字节)不同。
字面量写法与类型后缀
Rust 支持多种进制的字面量,还可以在数字中插入下划线提升可读性:
| 字面量形式 | 示例 |
|---|---|
| 十进制 | 98_222 |
| 十六进制 | 0xFF |
| 八进制 | 0o77 |
| 二进制 | 0b1111_0000 |
字节(仅限 u8) | b'A' |
fn main() {
let decimal = 255; // 十进制
let hex = 0xFF; // 十六进制(0x 前缀)
let octal = 0o377; // 八进制(0o 前缀)
let binary = 0b1111_1111; // 二进制(0b 前缀)
let million = 1_000_000; // 下划线只是视觉分隔符
let byte: u8 = b'A'; // 字节字面量,等价于 65u8
// 类型后缀:直接写在数字后面指定类型
let typed: u32 = 255u32;
let also_typed = 255_u8; // 下划线可以放在后缀前
println!("{} {} {} {} {} {} {} {}",
decimal, hex, octal, binary, million, byte, typed, also_typed);
}类型后缀的用途:当 Rust 无法从上下文推断类型时,直接在字面量后写类型:42u8、3.14f32、1000i64。
复合类型
元组
元组可以把不同类型的多个值打包在一起,用圆括号 () 包裹:
fn main() {
let t = (1i32, true, 'x', 3.14f64);
// 用 .索引 访问元素(注意:是 .0 不是 [0])
println!("第一个: {}", t.0);
println!("第二个: {}", t.1);
// 整个元组用 {:?} 打印
println!("完整元组: {:?}", t);
}元组用
.索引访问,不能 用t[0]——这是元组和数组的重要区别。
元组解构与函数返回值
解构(destructure)可以把元组的每个值绑定到独立变量:
fn min_max(numbers: &[i32]) -> (i32, i32) {
// 函数返回元组,实现多返回值
(numbers.iter().copied().min().unwrap(),
numbers.iter().copied().max().unwrap())
}
fn main() {
let point = (10, 20);
let (x, y) = point; // 解构:把 10 绑定给 x,20 绑定给 y
println!("x={}, y={}", x, y);
// 元组作为多返回值
let nums = [3, 1, 4, 1, 5, 9, 2, 6];
let (min, max) = min_max(&nums);
println!("最小值={}, 最大值={}", min, max);
// 只有一个元素的元组需要尾随逗号以区分括号表达式
let single = (42,);
println!("单元素元组: {:?}", single);
println!("这只是括号: {:?}", (42)); // 这是 i32,不是元组
}数组
数组存储相同类型的多个值,长度编译时固定,存储在栈上:
fn main() {
// 类型标注格式:[元素类型; 长度]
let xs: [i32; 5] = [1, 2, 3, 4, 5];
// 用相同值初始化:[初始值; 长度]
let zeros: [i32; 3] = [0; 3]; // [0, 0, 0]
println!("第一个: {}", xs[0]);
println!("最后一个: {}", xs[xs.len() - 1]);
println!("长度: {}", xs.len());
println!("zeros: {:?}", zeros);
// 越界访问会 panic(运行时错误)
// println!("{}", xs[10]); // panic: index out of bounds
}数组长度是类型的一部分:
[i32; 5]和[i32; 6]是两种不同类型,不能互相赋值。
切片
切片 &[T] 是对数组(或数组的一段)的引用视图,大小在编译时不固定:
fn sum_slice(slice: &[i32]) -> i32 {
// 函数接受切片,可以处理任意长度的数组
slice.iter().sum()
}
fn main() {
let arr = [1, 2, 3, 4, 5];
// 借用整个数组为切片
let all: &[i32] = &arr;
println!("全部: {:?}, 长度: {}", all, all.len());
// 借用数组的一部分(半开区间 [1, 4),即索引 1、2、3)
let part = &arr[1..4];
println!("部分: {:?}", part);
// 函数可以接受任意长度的切片
println!("全部的和: {}", sum_slice(&arr));
println!("部分的和: {}", sum_slice(&arr[1..4]));
}数组 vs 切片的核心区别:
数组 [T; N] | 切片 &[T] | |
|---|---|---|
| 大小 | 编译时固定 | 运行时动态 |
| 是否拥有数据 | 是 | 否(只是引用) |
| 用途 | 存储固定长度序列 | 函数参数、传递数据视图 |
运算符
常用运算符
Rust 支持算术、布尔、位运算等常见运算符,用法与 C 语言基本一致:
fn main() {
// 算术运算
println!("5 + 3 = {}", 5i32 + 3);
println!("5 - 3 = {}", 5i32 - 3);
println!("1 - 2 = {}", 1i32 - 2); // 有符号才能得到负数
// 整数除法:结果向零截断(不是向下取整)
println!("5 / 2 = {}", 5i32 / 2); // 2,不是 2.5
println!("2 / 3 = {}", 2i32 / 3); // 0!注意这个陷阱
println!("43 % 5 = {}", 43i32 % 5); // 取余 = 3
// 布尔运算(短路求值)
println!("true && false = {}", true && false);
println!("true || false = {}", true || false);
// 位运算
println!("0b0011 & 0b0101 = {:04b}", 0b0011u32 & 0b0101); // AND
println!("0b0011 | 0b0101 = {:04b}", 0b0011u32 | 0b0101); // OR
println!("0b0011 ^ 0b0101 = {:04b}", 0b0011u32 ^ 0b0101); // XOR
println!("1 << 3 = {}", 1u32 << 3); // 左移
println!("16 >> 2 = {}", 16u32 >> 2); // 右移
}无符号整数减法陷阱:
1u32 - 2u32会 panic(溢出)。如果可能出现负数,应使用有符号类型。
优先级完整列表
当一个表达式里有多个运算符时,谁先算、谁后算由优先级决定。优先级高的运算符先计算,同级从左到右。
Rust 的运算符优先级与 C 语言高度相似,如果你有 C/C++ 背景,大部分直觉可以直接沿用。主要差异在于:Rust 没有 C 的三目运算符 ? :,as 类型转换替代了 C 的强制类型转换,以及比较运算符不支持链式写法。
下表从高到低排列 Rust 的运算符优先级:
| 优先级 | 运算符 | 说明 | 结合性 |
|---|---|---|---|
| 1(最高) | expr.field、expr.method() | 字段访问、方法调用 | 左到右 |
| 2 | expr[index] | 索引 | 左到右 |
| 3 | expr? | 错误传播 | — |
| 4 | -expr、!expr、*expr、&expr、&mut expr | 一元运算符(取负、逻辑非、解引用、借用) | 右到左 |
| 5 | as | 类型转换 | 左到右 |
| 6 | *、/、% | 乘、除、取余 | 左到右 |
| 7 | +、- | 加、减 | 左到右 |
| 8 | <<、>> | 位移 | 左到右 |
| 9 | & | 位与(Bitwise AND) | 左到右 |
| 10 | ^ | 位异或(Bitwise XOR) | 左到右 |
| 11 | | | 位或(Bitwise OR) | 左到右 |
| 12 | ==、!=、<、>、<=、>= | 比较运算符 | 要求括号 |
| 13 | && | 逻辑与(短路) | 左到右 |
| 14 | || | 逻辑或(短路) | 左到右 |
| 15 | ..、..= | 区间 | — |
| 16 | =、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>= | 赋值与复合赋值 | 右到左 |
| 17(最低) | return、break、闭包 | 控制流表达式 | — |
比较运算符(==、!=、<、> 等)不支持链式比较:
1 < x < 10在 Rust 中是非法的,必须写成1 < x && x < 10。
常见场景示例
fn main() {
// 乘除优先于加减(和数学一样)
println!("{}", 2 + 3 * 4); // 14,不是 20
// 位运算优先级低于算术,常需括号
println!("{}", 1 + 2 & 3); // 先算 1+2=3,再 3&3=3
println!("{}", 1 + (2 & 3)); // 先算 2&3=2,再 1+2=3(不同!)
// 比较运算符优先级低于算术
println!("{}", 2 + 3 > 4); // 先算 2+3=5,再 5>4=true
// 逻辑与优先于逻辑或
println!("{}", true || false && false); // 先算 false&&false=false,再 true||false=true
// as 类型转换优先级较高
let x = 3.99_f64 as i32 + 1; // 先 as:3.99→3,再 3+1=4
println!("{}", x);
}最佳实践:多用括号
优先级规则记不住没关系——加括号让意图更清晰,比依赖优先级更好:
fn main() {
let a = 5;
let b = 3;
let c = 2;
// 不清晰:读者要记忆优先级
let r1 = a + b * c & 0xFF;
// 清晰:括号明确每步意图
let r2 = (a + (b * c)) & 0xFF;
println!("{} {}", r1, r2); // 结果相同,但 r2 的写法更易维护
}Clippy 会提示:当表达式中混合了不同类的运算符(如算术和位运算)而没有括号时,
cargo clippy会建议你加上括号,这是 Rust 社区推荐的风格。
练习题
整数类型的范围
默认整数类型
元组的访问方式
fn main() {
let t = (10, "hello", 3.14);
println!("{}", t.1);
}
char 的特点
数组类型标注
数组越界
fn main() {
let arr = [1, 2, 3];
println!("{}", arr[5]);
}
整数除法
fn main() {
let x: i32 = 7 / 2;
println!("{}", x);
}
整型溢出行为
优先级计算
fn main() {
println!("{}", 2 + 3 * 4 - 1);
}
单元类型
fn say_hello() {
println!("Hello!");
}
fn main() {
let x = say_hello();
println!("{:?}", x);
}
编程练习 1
下面函数接受一个坐标元组并计算两点之间的距离,但有一处访问方式写错了,请修复:
fn distance(p1: (f64, f64), p2: (f64, f64)) -> f64 {
let dx = p1[0] - p2[0]; // 错误的访问方式
let dy = p1[1] - p2[1]; // 错误的访问方式
(dx * dx + dy * dy).sqrt()
}
fn main() {
let a = (0.0, 0.0);
let b = (3.0, 4.0);
println!("{}", distance(a, b));
}编程练习 2
下面函数想计算百分比,但因为整数除法导致结果总是 0.0,请修复使其输出正确结果。
fn percentage(part: i32, total: i32) -> f64 {
(part / total * 100) as f64
}
fn main() {
println!("{:.1}", percentage(1, 3));
println!("{:.1}", percentage(2, 5));
}