跳转至

1. 基础语法篇

对应章节:第1-4章。
本篇掌握最基础的语法、类型、控制流与所有权初体验。
动手前请先阅读 依赖管理 一节,掌握 cargo new / cargo add / cargo search 的用法。


项目 1:猜数字游戏

对应章节:第1-2章

目标:掌握 Rust 最基础的语法与交互式程序。

创建项目

cargo new guessing_game
cd guessing_game
cargo add rand   # 添加随机数 crate(自动用最新版)

功能要求

  1. 生成 1~100 的随机数
  2. 循环读取用户输入
  3. 每次猜测后提示"大了/小了"
  4. 猜中后结束并提示

知识点

知识点 说明
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;
            }
        }
    }
}

运行

cargo run

注意事项

  1. guess 两次绑定(shadowing):先用 String 存储输入,再 let guess: u32 重新绑定为数字——这利用了变量遮蔽,不必起新名字。
  2. &secret_number 必须传引用:cmp 需要借用,否则 secret_number 被移走。
  3. Err(_) 用 continue:非数字输入跳过本次循环,不崩溃。
  4. .expect() vs 真正的错误处理:这里先了解 panic,第9章会学规范的 Result 处理。
  5. ⚠️ rand 0.10 的 API 变化:网上旧教程(含《The Book》早期版本)用的是 rand 0.8 的写法 rand::thread_rng().gen_range(...),在 0.10 中会报 cannot find function thread_rng。新版对应关系:
  6. rand::thread_rng() → rand::rng()
  7. .gen_range(1..=100) → .random_range(1..=100)
  8. use rand::Rng; → use rand::RngExt;(或 use rand::prelude::*;) 遇到 API 对不上时,去 https://docs.rs/rand 查当前版本的文档即可。

项目 2:命令行计算器

对应章节:第3章

目标:从命令行读取参数,实现四则运算。

创建项目

cargo new calculator
cd calculator

功能要求

cargo run -- 12 + 34
cargo run -- 10 / 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:输入非数字直接崩溃

注意事项

  1. args[0] 是程序路径:所以参数从 args[1] 开始,四则运算需要 len() == 4。
  2. op.as_str() 才能 match:op 是 &String,match 字符串需要 &str,用 .as_str() 转换。
  3. 除零检查:浮点数除零不 panic,会得到 inf,必须显式判断。
  4. match 表达式返回值:每个分支返回 f64,整体赋给 result。
  5. unwrap() 的代价:解析失败会直接 panic 并打印一长串错误信息,对用户不友好。试试用 .expect("自定义提示") 替代 .unwrap()——它同样会崩溃,但能换成你指定的提示语(参考项目1的 .expect("读取失败"))。想做到"提示后不崩溃",需要第9章的 Result 知识,届时可回头重构本项目。

项目 3:文件行数统计器

对应章节:第4章(所有权)

目标:读取文件并统计行数、单词数、字符数,初步体会所有权与借用。

创建项目

cargo new count
cd count

功能要求

cargo run -- poem.txt

输出文件的总行数、单词数、字符数。

知识点

知识点 说明
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);
}

运行

echo "hello world" > test.txt
cargo run -- test.txt
# 行数:1,单词数:2,字符数:11

注意事项

  1. .chars() 而不是 .bytes():中文字符多字节,用 bytes() 会数出字节数;chars() 才按字符计。
  2. 所有权与借用:content 是 String,.lines() 等返回借用的迭代器,content 全程活着,所以能连续调用多个 .count()。
  3. .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章可以自己合并两者。

评论