基础语法
这一课你将能做到
- 用 let / let mut / shadowing 管理变量,理解「默认不可变」的设计
- 写函数、用 if / loop / while / for 组织控制流,把 if 当表达式用
- 完成一个统计判题耗时的 CLI 小工具
为什么要学
语法是所有后续内容的载体。Rust 的语法表面像 C,骨子里偏函数式:几乎一切都是表达式,类型推导无处不在,编译器报错友好到近乎手把手。这节课把地基打平,后面所有权、错误处理这些硬菜才咽得下去。
和 Java 对照着学最快:变量声明像 var,常量像 final,但「默认不可变」这条设计,把 Java 里靠自觉的 best practice 变成了强制。
先建立图像
Java: int total = 0; final int MAX = 100;
Rust: let mut total = 0; const MAX: u32 = 100;
Java: var name = "judge"; // 类型推断
Rust: let name = "judge"; // 类型推断 + 默认不可变
核心心智模型:let 绑定的是「名字到值」的关系,绑定默认不可变——想改值,显式声明 mut;想换绑新值,直接再 let(shadowing)。
跟着做
步骤 1:变量、可变性与 shadowing
cargo new basic-demo
cd basic-demo
把 src/main.rs 换成:
fn main() {
let submit_count = 10; // 不可变绑定
// submit_count = 11; // 编译错误:不可变变量不能二次赋值
let mut total_ms = 0u64; // mut 声明可变
total_ms += 150;
let submit_count = submit_count * 2; // shadowing:换绑新值,连类型都可以变
println!("提交数 {submit_count},总耗时 {total_ms} ms");
}
运行 cargo run。注意 shadowing 不是修改,是旧名字被新绑定遮蔽,编译器把它当两个变量。
步骤 2:基本类型与推导
fn main() {
let a = 42; // 推导为 i32(整型默认)
let b: u64 = 18_446_744_073_709_551_615; // 显式标注,_ 是数字分隔符
let pi = 3.14159; // f64(浮点默认)
let ok = true; // bool
let letter = '判'; // char 是 4 字节 Unicode 标量
let pair = ("cpp", 1000); // 元组:异构定长集合
let arr = [3, 1, 4, 1, 5]; // 数组:定长,栈上
let (lang, limit) = pair; // 元组解构
println!("{a} {b} {pi} {ok} {letter} {lang} {limit} {}", arr[2]);
}
整型家族:i8/i16/i32/i64/i128/isize 与对应 u 系列。判题内存计数、跨语言协议这类场景明确标 u64,其余交给推导。
步骤 3:函数与返回值
fn grade(score: u32) -> &'static str {
if score >= 90 { "A" } else if score >= 60 { "P" } else { "F" }
} // if 是表达式,不带分号的分支值直接作为返回值
fn add_time(total: &mut u32, delta: u32) {
*total += delta; // 不返回值时,隐式返回单元类型 ()
}
fn main() {
let mut total = 0;
add_time(&mut total, 120);
add_time(&mut total, 80);
println!("耗时 {total} ms,评级 {}", grade(95));
}
两个关键点:函数最后一行「不带分号的表达式」自动作为返回值,写了分号就变成 ();参数类型必须显式标注,且没有重载。
步骤 4:控制流全家桶
fn main() {
// for + Range
for case_id in 1..=3 { // 1..=3 闭区间,1..4 半开区间
println!("跑第 {case_id} 个样例");
}
// while
let mut remaining = 8;
while remaining > 0 { remaining /= 2; }
// loop:能带返回值的无限循环
let mut n = 1u64;
let doubled = loop {
n *= 2;
if n > 1000 { break n; } // break 带值,作为 loop 表达式的值
};
println!("{remaining} / {doubled}");
}
Rust 的 if 条件必须是严格 bool,if x = 5 这种 Java 里的手滑直接编译报错。
步骤 5:综合小工具——判题耗时统计
fn classify(avg: u64) -> &'static str {
if avg < 100 { "S" } else if avg < 500 { "A" } else { "B" }
}
fn main() {
let times_ms = [12, 8, 45, 3, 21]; // 各测试点耗时(毫秒)
let mut total: u64 = 0;
let mut max: u64 = 0;
for &t in times_ms.iter() {
total += t as u64;
if t as u64 > max { max = t as u64; }
}
let avg = total / times_ms.len() as u64;
println!("样本数 {}", times_ms.len());
println!("总耗时 {total} ms,最慢单点 {max} ms,平均 {avg} ms");
println!("性能评级:{}", classify(avg));
}
运行后改几个数字观察输出。数组的 .iter() 遍历和 as 转换先照抄,分别在第 8、9 课展开。
讲清楚要点
- 默认不可变:
let绑定不可变,mut是显式 opt-in。「谁在什么时候改了数据」在代码里一眼可见,也是后续并发安全的基石。 - shadowing 不等于 mut:shadowing 生成新绑定,可以顺便改类型(
let x = "42"; let x: u32 = x.parse().unwrap());mut 只是原地改值。 - 一切皆表达式:if、loop、代码块
{}都有值。Rust 没有三元运算符,因为let x = if c { a } else { b }就是三元。 - 常量与不可变变量是两回事:
const MAX: u32 = 100;编译期内联、必须标类型;let绑定有作用域、有所有权语义。 - 整型溢出:debug 构建溢出直接 panic,release 构建按位回绕。要明确语义用
checked_add/wrapping_add。
你可能会懵的地方
-
为什么变量默认不可变? 可变状态是并发 bug 与推理负担的主要来源。默认不可变加显式 mut,让可变性成为 review 一眼能盯住的点。
-
shadowing 和 mut 怎么选? 类型不变、就地累加用 mut;需要转换类型或重新解释语义用 shadowing(先拿原始字符串再 parse 成数字,同名换绑最顺手)。
-
u32 和 u64 怎么选? 默认 i32 够用;内存字节数、时间戳、大计数用 u64。拿不准就 u64;Rust 收窄转换也必须写
as,不存在 Java 的静默截断惊喜。 -
函数没有 return 会不会看漏返回值? Rust 惯例:最后表达式即返回值,只有提前返回才写
return。看到函数末行没分号,它就是返回值。 -
&'static str是什么? 生命周期标注,表示这个字符串引用整个程序期间有效。所有字符串字面量都是'static,第 4 课展开。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 不可变变量二次赋值 | error[E0384] | 声明 let mut 或用 shadowing |
| 函数末行多写分号 | 返回 () 类型不匹配 | 返回值表达式结尾不带分号 |
i32 + u64 直接混算 | error[E0308] 类型不匹配 | a as u64 + b 显式转换 |
if total { } 把数字当条件 | 条件必须严格 bool | if total > 0 { } |
| 变量声明后不用 | warning: unused variable | 改名 _x 消音或删除 |
卡点急救
- 一堆 warning 但能跑:warning 不阻断运行,
cargo fix能自动修掉大部分(比如提示加 mut),保持零警告是好习惯。 - error[E0308] mismatched types:编译器会同时打印期望类型与实际类型,照着改即可;读懂这个错误码就学会了 Rust 排错的姿势。
- 数值算出来不对:检查整型除法截断(
5/2 = 2)与溢出回绕;浮点比较用(a - b).abs() < 1e-9,别用==。
面试常问
-
Rust 变量默认不可变带来什么好处? 可变状态被显式化,读写边界清晰,是编译期并发安全(Send/Sync 体系)的前置条件;Java 里
final是可选的好习惯,Rust 把它升级为语言默认。 -
「一切皆表达式」对代码风格意味着什么? 减少临时变量与命令式空转:if/match/loop 直接作为值赋给绑定,控制流与数据流合一,代码更接近「数据变换」的描述而非指令序列。
-
debug 和 release 下整型溢出行为有何不同? debug 溢出 panic(尽早暴露 bug),release 按二进制回绕(保性能)。依赖回绕语义必须显式用
wrapping_*系列,把意图写进代码。
自测三问
- shadowing 和
mut的区别是什么?各举一个适用场景。 let x = if a > 0 { 1 } else { -1 };为什么合法?1..4和1..=4分别生成什么序列?
小结一句话
变量默认不可变、一切皆表达式、类型推导加显式边界——Rust 基础语法把 Java 的最佳实践升级成了语言强制。
延伸
- The Rust Book ch03:通用编程概念
- 下一课:所有权与借用——Rust 的灵魂,也是与 Java 差异最大的部分。