第2章:语言基础 —— 变量、类型、函数与流程控制
约 7 分钟 · 更新于 2026-09-01
第2章:语言基础 —— 变量、类型、函数与流程控制
本章通过一个“批次质量报告”程序串起基础语法。阅读重点不是记忆关键字,而是追踪值怎样被绑定、转换并流经表达式:哪些变化必须显式声明,哪些类型由上下文推导,哪些代码块会产生结果。
一、从一条数据流开始
先阅读一个完整但不复杂的程序:
rust
fn pass_rate(results: &[bool]) -> f64 {
let passed = results.iter().filter(|ok| **ok).count();
passed as f64 / results.len() as f64
}
fn level(rate: f64) -> &'static str {
if rate >= 0.95 {
"excellent"
} else if rate >= 0.80 {
"stable"
} else {
"needs-review"
}
}
fn main() {
let checks = [true, true, false, true, true];
let rate = pass_rate(&checks);
println!("rate={rate:.0%}, level={}", level(rate));
}
不要急着逐行背诵,先找出数据流:数组进入函数,函数借用数组并计算比例,if 产生分类文本,最后格式化输出。接下来的章节会拆开其中的绑定、类型、表达式、转换和控制流。
阅读基础语法的顺序
先问“这个表达式产生什么值”,再问“值是什么类型”,最后问“这个绑定之后是否允许改变”。这个顺序比按关键字背定义更容易迁移到真实代码。
二、变量绑定:默认不可变
rust
fn main() {
let score = 90;
println!("{score}");
}
默认情况下不能再次赋值:
rust
let score = 90;
score = 95; // 编译错误
确实需要改变时显式使用 mut:
rust
let mut score = 90;
score += 5;
println!("{score}");
不可变默认值让变化变得可见。读到 let total = 100,可以假设后面不会重新赋值;读到 let mut total = 100,则知道后续逻辑会改变它。
mut 不允许变量随意换类型:
rust
let mut spaces = " ";
spaces = spaces.len(); // &str 不能变成 usize
三、遮蔽:复用名字,创建新绑定
新的 let 可以遮蔽旧变量:
rust
fn main() {
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().expect("请输入整数");
println!("解析结果:{input}");
}
三个 input 是三个不同绑定,因此可以从原始文本变为去空白文本,再变为整数。
| 方式 | 新绑定 | 可改变类型 | 适合场景 |
|---|
| let mut value | 否 | 否 | 同一含义的值持续变化 |
| 再次 let value | 是 | 是 | 同一概念经历转换步骤 |
作用域内部的遮蔽不会改变外层绑定:
rust
let x = 5;
{
let x = x * 2;
println!("内部:{x}");
}
println!("外部:{x}");
四、常量、忽略值与解构
常量使用 const,必须标注类型:
rust
const MAX_RETRIES: u32 = 3;
const TIMEOUT_MS: u64 = 5_000;
常量不能使用 mut,值必须能在编译期求出,名称通常使用全大写蛇形命名。
明确暂时不用的变量可加下划线;单独的 _ 表示忽略值:
rust
let _result = 42;
for _ in 0..3 {
println!("执行一次");
}
let 还能解构复合值:
rust
let user = (1001_u64, "Alice", true);
let (id, name, active) = user;
println!("id={id}, name={name}, active={active}");
五、静态类型、推导与标注
Rust 必须在编译期知道每个值的类型,但通常可以推导:
rust
let count = 10; // i32
let ratio = 0.75; // f64
let enabled = true; // bool
let letter = 'R'; // char
上下文不足时需要标注。例如字符串可以解析成多种数字:
rust
let port: u16 = "8080".parse().expect("端口必须是数字");
let backup = "8081".parse::<u16>().expect("端口必须是数字");
适合显式标注的场景包括:编译器无法推导、函数边界需要表达契约、数值范围有业务含义,或标注能明显提升可读性。
六、整数:先选范围,再选溢出策略
| 位数 | 有符号 | 无符号 |
|---|
| 8 | i8 | u8 |
| 16 | i16 | u16 |
| 32 | i32 | u32 |
| 64 | i64 | u64 |
| 128 | i128 | u128 |
| 指针宽度 | isize | usize |
普通整数默认用 i32,集合长度与索引常用 usize。需要表达负数时不要选无符号类型。
rust
let population = 1_400_000_000_u64;
let mask = 0b1111_0000_u8;
let color = 0xff_u8;
Rust 不随意执行隐式数值转换:
rust
let small: u16 = 500;
let large: u32 = 1_000;
let total = u32::from(small) + large;
可能溢出时,应明确策略:
rust
let max = u8::MAX;
assert_eq!(max.checked_add(1), None);
assert_eq!(max.wrapping_add(1), 0);
assert_eq!(max.saturating_add(1), u8::MAX);
let (value, overflowed) = max.overflowing_add(1);
assert_eq!((value, overflowed), (0, true));
- checked_*:溢出返回 None
- wrapping_*:按补码环绕
- saturating_*:限制在边界值
- overflowing_*:返回结果和溢出标记
七、浮点、布尔、字符与单元类型
Rust 有 f32 与默认的 f64。二进制浮点数无法精确表示许多十进制小数:
rust
let actual = 0.1_f64 + 0.2;
let expected = 0.3_f64;
assert!((actual - expected).abs() < 1e-10);
不要草率地用 == 比较计算结果。货币通常应使用最小货币单位的整数,或专门的十进制定点库。未定义运算可能产生 NaN,可用 .is_nan() 检查。
bool 只有 true 和 false。if 条件必须产生布尔值,不能把整数或字符串隐式当作真假。
char 表示一个 Unicode 标量值,占 4 字节:
rust
let latin = 'R';
let chinese = '中';
let text = "Rust";
字符用单引号,字符串用双引号。
() 是单元类型。没有显式返回值的普通函数会返回它:
rust
fn log_message(message: &str) {
println!("{message}");
}
八、数组与范围
数组长度固定,元素类型相同:
rust
let numbers = [10, 20, 30, 40];
let zeros = [0; 5];
println!("{} {}", numbers[0], zeros.len());
数组类型包含长度,例如 [i32; 4]。
范围常用于循环:
rust
for i in 1..5 { // 1、2、3、4
println!("{i}");
}
for i in 1..=5 { // 包含 5
println!("{i}");
}
九、语句与表达式:分号会丢弃值
let 绑定是语句;算术、函数调用、if 和代码块可以是表达式。表达式求值后产生值。
rust
fn main() {
let area = {
let width = 4;
let height = 6;
width * height
};
println!("面积:{area}");
}
代码块最后的 width * height 没有分号,因此成为代码块的值。加上分号后,代码块返回 ()。
分号不是纯格式
当函数提示“expected i32, found ()”时,优先检查尾表达式是否多了分号。
let 自身不是可继续赋值的表达式,因此不能写 let b = (let a = 8);。
十、函数:参数显式,结果可以隐式返回
rust
fn add(left: i32, right: i32) -> i32 {
left + right
}
fn main() {
let result = add(20, 22);
println!("{result}");
}
函数参数必须标注类型,-> i32 声明返回类型,最后一个无分号表达式作为返回值。
提前退出使用 return:
rust
fn absolute(value: i32) -> i32 {
if value < 0 {
return -value;
}
value
}
常见风格是提前退出用 return,正常结果用尾表达式。永不返回的函数可标注 !:
rust
fn stop(message: &str) -> ! {
panic!("{message}");
}
十一、if 是表达式
rust
fn main() {
let score = 86;
let result = if score >= 90 {
"优秀"
} else if score >= 60 {
"通过"
} else {
"未通过"
};
println!("{result}");
}
所有可能返回的分支必须类型兼容:
rust
let value = if condition { 1 } else { 2 }; // 正确
let value = if condition { 1 } else { "two" }; // 错误
Rust 不要求条件外加圆括号,但条件必须是 bool。
十二、for:遍历集合的首选
rust
let values = [3, 5, 8, 13];
for value in values {
println!("{value}");
}
需要索引时使用 .enumerate():
rust
let values = ["Rust", "Go", "Python"];
for (index, value) in values.iter().enumerate() {
println!("第 {} 项:{value}", index + 1);
}
只重复固定次数且不使用计数值:
rust
for _ in 0..3 {
println!("重试");
}
能直接遍历时不要先维护下标。手动索引更容易越界,也没有直接表达“访问每个元素”。
十三、while、loop 与循环控制
条件驱动的循环使用 while:
rust
let mut remaining = 3;
while remaining > 0 {
println!("{remaining}");
remaining -= 1;
}
loop 表示无条件循环,还可以通过 break 返回值:
rust
let mut count = 0;
let doubled = loop {
count += 1;
if count == 10 {
break count * 2;
}
};
break 结束整个循环,continue 跳过本轮剩余代码:
rust
for number in 1..=6 {
if number % 2 == 0 {
continue;
}
if number > 5 {
break;
}
println!("{number}");
}
遍历集合优先 for,条件循环用 while,需要无限循环或循环返回值时用 loop。
十四、格式化输出
rust
fn main() {
let language = "Rust";
print!("正在学习 ");
println!("{language}");
let message = format!("欢迎进入 {language}");
eprintln!("调试信息:{message}");
}
- print!:输出但不换行
- println!:输出并换行
- format!:不打印,返回 String
- eprintln!:输出到标准错误
{} 面向用户可读输出,{:?} 面向调试:
rust
let point = (3, 4);
println!("{point:?}");
println!("{point:#?}");
常用格式:
rust
let pi = 3.1415926;
let value = 27;
println!("{pi:.2}");
println!("{value:05}");
println!("{value:#b}");
println!("{value:#x}");
不必一次背完格式规则,先掌握普通输出、调试输出、精度和宽度。
十五、综合示例:成绩统计
rust
fn grade(score: u32) -> &'static str {
if score >= 90 { "A" }
else if score >= 80 { "B" }
else if score >= 60 { "C" }
else { "D" }
}
fn main() {
const PASS_LINE: u32 = 60;
let scores = [95, 72, 88, 59, 81];
let mut total = 0_u32;
let mut passed = 0;
for (index, score) in scores.iter().enumerate() {
total += score;
if *score >= PASS_LINE {
passed += 1;
}
let result = if *score >= PASS_LINE { "通过" } else { "未通过" };
println!("学生 {}:{score} 分,等级 {},{result}", index + 1, grade(*score));
}
let mean = total as f64 / scores.len() as f64;
println!("平均分:{mean:.1},通过人数:{passed}");
}
为什么迭代时会看到引用、为什么某些集合遍历后仍能使用,将在下一章通过所有权和借用解释。
十六、常见误区
- 所有变量都加 mut:只有确实需要改变时才加
- 混淆遮蔽与修改:再次 let 创建的是新绑定
- 尾表达式加分号:函数可能因此返回 ()
- 把整数当布尔条件:应明确写 count > 0
- 直接比较浮点计算结果:应使用符合业务精度的误差范围
- 遍历时先想到下标:优先直接迭代,需要索引再 .enumerate()
- 用 as 随意缩窄:可能越界时优先 u8::try_from(value) 等可检查转换
十七、实践练习
练习一:运费分级
编写 shipping_fee(weight: f64, express: bool) -> f64。用 if 表达式计算基础费用,要求所有分支返回同一类型,并处理重量小于等于零的输入。
练习二:批次编号
遍历 100..=110:能被 3 整除输出 tri,能被 5 整除输出 penta,同时满足则输出 tri-penta,否则输出编号。先用 for,再尝试用 match 搭配元组布尔值改写。
练习三:统计有效值
给定 [12, -1, 8, 0, 21, -3],忽略负数,统计有效值数量、总和与最大值。要求最大值使用 Option<i32> 表达“可能不存在”。
练习四:分号实验
分别让代码块的尾部是 value + 1 和 value + 1;,观察类型如何从整数变成 ()。写出你对“语句执行动作、表达式产生值”的解释。
练习五:显式数值策略
把一个 u16 与一个 u32 相加,并分别使用 checked_add、saturating_add 演示溢出策略。不要用未经说明的 as 缩窄转换。
十八、总结
- let 创建绑定,默认不可变;mut 表示同一绑定的值会变化,遮蔽则创建新绑定。
- Rust 要求类型在编译期确定,但会尽量从上下文推导;边界和业务范围不清时应显式标注。
- 数值类型之间不随意隐式转换,溢出与缩窄都应选择明确策略。
- 代码块、if 与 loop 都能产生值;尾表达式后的分号会把结果丢弃为 ()。
- for 适合遍历,while 适合条件循环,loop 适合显式退出或返回值。
- 阅读 Rust 代码时,应同时追踪值、类型和可变性,而不是孤立识别关键字。
下一章把“值如何流动”推进到资源层面:赋值与传参何时复制、何时移动,引用又如何在不接管资源的情况下访问数据。
请继续阅读:第3章:所有权 —— 借用、切片与内存模型。
- 第1章:开篇 —— Rust 为什么值得学与环境搭建
- 第3章:所有权 —— 借用、切片与内存模型