1. 基础语法篇¶
对应章节:第1-4章。
本篇掌握最基础的语法、类型、控制流与所有权初体验。
动手前请先阅读 依赖管理 一节,掌握cargo new/cargo add/cargo search的用法。
项目 1:猜数字游戏¶
对应章节:第1-2章
目标:掌握 Rust 最基础的语法与交互式程序。
创建项目¶
功能要求¶
- 生成 1~100 的随机数
- 循环读取用户输入
- 每次猜测后提示"大了/小了"
- 猜中后结束并提示
知识点¶
| 知识点 | 说明 |
|---|---|
let mut |
可变绑定(默认不可变) |
String::new() |
创建空字符串 |
io::stdin().read_line() |
读取一行输入 |
.trim() |
去除首尾空白 |
.parse() |
字符串转数字 |
match + Ordering |
比较分支 |
loop + break |
无限循环退出 |
rand crate |
随机数生成 |
| 变量遮蔽(shadowing) | 同名变量重新绑定 |
代码¶
依赖已由 cargo add rand 添加(自动写入最新版本,形如 rand = "0.10"),无需手动编辑 Cargo.toml。
src/main.rs:
use std::io;
use std::cmp::Ordering;
use rand::RngExt; // rand 0.10+ 中 random_range 等方法定义在 RngExt trait 中
fn main() {
println!("猜数字游戏!");
// rand 0.10+ 的 API:rand::rng() 替代旧版 rand::thread_rng()
// random_range(1..=100):生成闭区间 [1, 100] 的随机数(替代旧版 gen_range)
let secret_number = rand::rng().random_range(1..=100);
loop {
println!("请输入你的猜测:");
// 每次循环都要新建 String(read_line 是追加写入)
let mut guess = String::new();
io::stdin().read_line(&mut guess)
.expect("读取失败"); // expect:失败直接 panic
// 变量遮蔽:把 guess 从 String 重新绑定为 u32
// 类型标注告诉 parse 解析成 u32
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue, // 输入非数字则重来
};
match guess.cmp(&secret_number) {
Ordering::Less => println!("太小了!"),
Ordering::Greater => println!("太大了!"),
Ordering::Equal => {
println!("你猜对了!");
break;
}
}
}
}
运行¶
注意事项¶
guess两次绑定(shadowing):先用String存储输入,再let guess: u32重新绑定为数字——这利用了变量遮蔽,不必起新名字。&secret_number必须传引用:cmp需要借用,否则 secret_number 被移走。Err(_)用continue:非数字输入跳过本次循环,不崩溃。.expect()vs 真正的错误处理:这里先了解 panic,第9章会学规范的Result处理。- ⚠️ rand 0.10 的 API 变化:网上旧教程(含《The Book》早期版本)用的是 rand 0.8 的写法
rand::thread_rng().gen_range(...),在 0.10 中会报cannot find function thread_rng。新版对应关系: rand::thread_rng()→rand::rng().gen_range(1..=100)→.random_range(1..=100)use rand::Rng;→use rand::RngExt;(或use rand::prelude::*;) 遇到 API 对不上时,去 https://docs.rs/rand 查当前版本的文档即可。
项目 2:命令行计算器¶
对应章节:第3章
目标:从命令行读取参数,实现四则运算。
创建项目¶
功能要求¶
输出计算结果;非法参数或除零给出友好提示。
知识点¶
| 知识点 | 说明 |
|---|---|
env::args() |
读取命令行参数 |
.collect::<Vec<String>>() |
收集为 Vec |
str::parse() |
字符串转数字 |
match 多分支 |
根据运算符分发 |
f64 |
浮点类型 |
op.as_str() |
&String 转 &str |
代码¶
src/main.rs:
use std::env;
fn main() {
// collect 收集所有命令行参数为 Vec<String>
let args: Vec<String> = env::args().collect();
// args[0] 是程序名(路径),所以"数字 运算符 数字"共需 4 个参数
if args.len() != 4 {
println!("用法:cargo run -- <数字> <运算符> <数字>");
return; // return 提前结束 main,不 panic
}
// parse 返回 Result<f64, ParseFloatError>
// 这里先用 unwrap:成功取出值,失败直接 panic(简化处理,见注意事项的改进练习)
let a: f64 = args[1].parse().unwrap();
let op = &args[2]; // 借用,不移动所有权
let b: f64 = args[3].parse().unwrap();
// match 表达式:每个分支都返回 f64,整个 match 赋给 result
let result = match op.as_str() {
"+" => a + b,
"-" => a - b,
"*" => a * b,
"/" => {
// 浮点除零不 panic 但会得到 inf,必须显式判断
if b == 0.0 {
println!("不能除以零!");
return;
}
a / b
}
_ => { println!("不支持的运算符:{}", op); return; }
};
println!("{} {} {} = {}", a, op, b, result);
}
运行¶
cargo run -- 12 + 34 # 12 + 34 = 46
cargo run -- 10 / 3 # 10 / 3 = 3.3333...
cargo run -- 1 / 0 # 不能除以零!
cargo run -- abc + 1 # panic:输入非数字直接崩溃
注意事项¶
args[0]是程序路径:所以参数从args[1]开始,四则运算需要len() == 4。op.as_str()才能 match:op是&String,match 字符串需要&str,用.as_str()转换。- 除零检查:浮点数除零不 panic,会得到
inf,必须显式判断。 match表达式返回值:每个分支返回f64,整体赋给result。unwrap()的代价:解析失败会直接 panic 并打印一长串错误信息,对用户不友好。试试用.expect("自定义提示")替代.unwrap()——它同样会崩溃,但能换成你指定的提示语(参考项目1的.expect("读取失败"))。想做到"提示后不崩溃",需要第9章的Result知识,届时可回头重构本项目。
项目 3:文件行数统计器¶
对应章节:第4章(所有权)
目标:读取文件并统计行数、单词数、字符数,初步体会所有权与借用。
创建项目¶
功能要求¶
输出文件的总行数、单词数、字符数。
知识点¶
| 知识点 | 说明 |
|---|---|
fs::read_to_string |
一次性读取整个文件为 String |
.lines() |
按行迭代(返回借用 &str) |
.split_whitespace() |
按空白分割单词 |
.chars() |
字符迭代(非字节) |
.count() |
迭代器计数 |
| 所有权与借用 | 借用不能超过数据存活 |
代码¶
src/main.rs:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() != 2 {
println!("用法:cargo run -- <文件名>");
return;
}
let filename = &args[1];
// read_to_string 返回 Result<String, io::Error>,expect 遇错 panic
let content = fs::read_to_string(filename)
.expect("读取文件失败");
// 这三处都是对 content 的借用迭代(lines/split_whitespace/chars 都返回借用)
// content 全程活着,所以可以连续调用
let line_count = content.lines().count();
let word_count = content.split_whitespace().count();
let char_count = content.chars().count();
println!("文件:{}", filename);
println!("行数:{}", line_count);
println!("单词数:{}", word_count);
println!("字符数:{}", char_count);
}
运行¶
注意事项¶
.chars()而不是.bytes():中文字符多字节,用bytes()会数出字节数;chars()才按字符计。- 所有权与借用:
content是String,.lines()等返回借用的迭代器,content全程活着,所以能连续调用多个.count()。 .expect()让文件不存在时直接报错退出——第9章会改成更优雅的错误处理。
另一种写法¶
同一个需求还有第二种实现,区别在于错误处理方式——用 match 处理 Result 而不是 .expect() 直接 panic:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let path = &args[1];
let content = fs::read_to_string(path);
match content {
Ok(text) => {
let line_count = text.lines().count();
let char_count = text.chars().count();
let word_count = text.split_whitespace().count();
println!("行数:{}", line_count);
println!("字符数:{}", char_count);
println!("单词数:{}", word_count);
}
Err(e) => {
println!("无法读取文件 {}: {}", path, e);
}
}
}
两种写法对比:
| count(上文) | wc(本节) | |
|---|---|---|
| 参数校验 | 检查 args.len() != 2 |
直接取 args[1](不带参数会 panic) |
| 文件读取失败 | .expect() panic 崩溃 |
match 的 Err(e) 分支打印友好提示 |
这种写法的用户体验更好(文件不存在不崩溃),但省掉了参数校验反而是退步——不带参数运行会因数组越界直接崩溃。最完善的版本是"参数校验 + match 错误处理",学完第9章可以自己合并两者。