🦀 Rust工程师养成记 Day3-猜价格小游戏

0 阅读12分钟

游戏规则

啰嗦了两节,相信你早就按捺不住上手实践的心情了,今天我们一起来看看一个猜价格的小游戏,在Rust中该怎么实现,顺便巩固一下我们之前两节学到的知识。

我们来捋一下游戏规则:小红开了一家新的服装店,服装的价格在100-500之间不等,他举行了一个活动,凡是进店的客户都可以猜一件服装的价格,每次猜,小红都会告诉猜价格的客户猜测的价格是太低还是太高了,五次内如果猜中,就可以将这件衣服免费领回家!

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧

新建项目

老规矩利用我们第一节做的模版直接一键三连🫣新建一个项目,边学边实践!

# 直接指定 --name 可以免除后续要输入项目名称的步骤
cargo generate EditYJ/rust-template --name guess-price-game
cd guess-price-game
pre-commit install

运行看看是否正常

好了,进入 src/main.rs 文件,开始我们愉快的编码吧!

获取猜测数据

我们首先需要接收用户的输入,以便为后续我们判断价格的高低提供依据:

// src/main.rs
use std::io;

fn main() {
	println!(":) 来吧!猜猜这个衣服的价格是多少?");
	
	println!("请输入你的猜测:");
	let mut guess = String::new();
	io::stdin()
		.read_line(&mut guess)
		.expect("获取你的猜测失败!");
	
	println!("你猜测的价格是: {}", guess.trim());
}

看到这段代码,经历前两节的学习你应该感觉又熟悉又陌生,下面我们来一行一行的过一下这些代码。

use、main与println!

use std::io;

use关键字类似于js中的importrequire的作用,这里导入了标准库std中的io库,io库中提供了很多关于输入输出相关的功能,包括接收用户输入的功能。

fn main() {
	println!(":) 来吧!猜猜这个衣服的价格是多少?");
	
	println!("请输入你的猜测:");

这三行你应该很熟悉,声明了主函数main,利用println!宏在控制台上打印了相关提示信息,提醒用户输入他对于衣服价格的猜测。

使用变量存储用户输入的值

let mut guess = String::new();

这里我们创建了一个guess变量,准备存储后续用户输入的价格,String::new()看起来很奇怪,我个人觉得可以这么理解:其实这句相当于Java中某个类调用类中的静态方法,newString 类型的一个关联函数。至于他为什么叫new而不叫其他的名称,这就是Rust中的一个约定,new名称的函数大部分都是用来实例化一个对象的。

连起来看就是String::new()创建了一个新的String实例,这个实例被绑定到了一个可变的变量guess上,接下来我们就要利用这个guess去接收用户的输入。

接收用户输入

io::stdin().read_line(&mut guess)

由于我们开头声明了use std::io,所以这里直接可以使用io,如果我们没有声明,那我们其实还可以写成这样:

std::io::stdin().read_line(&mut guess)

当然,聪明的你一定想到了,如果我开头这样声明use std::io::stdin是不是可以写成这样了:

stdin().read_line(&mut guess)

答案是正确的,这么一搞你是不是就彻底明白了use的基本用法了。

后面的read_line方法是用来读取用户输入的行数据的,当用户输入回车符后,read_line就会将数据存储到变量guess中,& 表示这个参数是一个 引用,这个概念我们后面会好好讲一下的,这边就先留个印象,记住是这么写就行了。

Result类型

如果你使用的vscode,并且按照第一节的要求安装好rust-analyzer插件的话,你应该能看到read_line方法的返回值是一个Result模样的东西:

Result是一个枚举,他是一个组合类型,他是Rust中进行错误处理的一个非常重要的数据类型,他的使用频率非常高,标准库中他的模样是这样的:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

Ok(T)包含了成功的值,Err(E)包含了操作失败的原因或方式的信息,和String类型一样,他的实例上也拥有各种各样的方法来帮助你处理错误。

expect方法就是Result上面方法的其中一个,如果Result的实例是Ok(T),那他就会原样返回Ok中的值,我们这个例子中这个usize类型的值是用户输入到标准输入中的字节数。如果Result的实例是Err(E),那程序就会立即中断,并输出对应的提示“获取你的猜测失败!”

运行一下

最后一行应该不用我说了,相信聪明的你一定会明白他是什么含义,我们现在运行cargo run,看看程序的运行结果是什么:

好了,现在我们已经获取到了用户的输入,是时候进入下一阶段了!

生成一个随机价格

安装rand库

现在我们将安装第一个rust库来帮助我们实现生成随机数的功能,运行下面的命令安装rand库:

cargo add rand

运行完成后,我们可以看到Cargo.toml中已经多了一个依赖:

对于rand的文档,你可以直接到crates.io查看,也可以本地运行cargo doc --open来查看rand的文档

你还可以通过Rust官方部署的文档站doc.rs查看。

尝试生成一个随机数

use std::io;
use rand::Rng;

fn main() {
    let mut rng = rand::rng();
    let secret_number = rng.random_range(100..=500);
    println!("秘密价格:{}", secret_number);
    println!("===== 欢迎来到猜价格游戏! =====");

    println!(":) 来吧!猜猜这个衣服的价格是多少?");
    println!("请输入你的猜测:");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("获取你的猜测失败!");

    println!("你猜测的价格是: {}", guess.trim());
}

我们来讲一讲这段新增的代码

首先我们新增了这一行

use rand::Rng;

Rng 是一个 trait(接口),它定义了随机数生成器应实现的方法,这里因为我们要使用这其中的random_range,要想使用random_range,此 trait 必须在作用域中。

然后我们新增了:

let mut rng = rand::rng();
let secret_number = rng.random_range(100..=500);

第一行代码生成了一个随机数生成器rng,它位于当前执行线程的本地环境中,并从操作系统获取随机数生成需要的种子,然后用过rng调用random_range方法生成100~500之前的随机整数赋值到secret_number

让我们多运行几次看看:

println!("秘密价格:{}", secret_number);打印出了随机数生成的价格,这个只是测试使用的,最终我们会删除这行,将生成的价格变成一个秘密。

比较用户猜测的和随机生成的数字

fn main() {
    // ...之前的代码
    
    println!("你猜测的价格是: {}", guess.trim());

    let guess: u32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => {
            println!("请输入一个有效的数字!");
            return;
        }
    };

    if guess < secret_number {
        println!("你猜的价格太低了!");
    } else if guess > secret_number {
        println!("你猜的价格太高了!");
    } else {
        println!("恭喜你!你猜对了!");
    }
}

因为获取的 guess 类型是String类型的,我们需要将他转成整型才好进行比较大小,我们来仔细分析一下上面的代码。

parse转换类型

let guess: u32 = match guess.trim().parse() {
	Ok(num) => num,
	Err(_) => {
		println!("请输入一个有效的数字!");
		return;
	}
};

细心的同学可能发现了,这个guess和我们之前的字符串类型的guess同名了,在其他语言中,例如js中是不允许这样做的,但是rust中是允许的,相当于guess记忆被删除,重新做人了!

上一节我们说过if可以当做表达式,其实match也可以当做表达式,通过指定guess要转成的类型u32,然后调用parse方法,就会自动将String类型的guess转成u32类型的guess,当parse返回的ResultOk的时候,guess就得到了转换后的值,如果是Err的话就会报错打印一行字"请输入一个有效的数字!"。

运行一下

后面的if比较的逻辑,相信大家都看得懂,我就不多赘述了。下面我们来运行一下cargo run看看效果:

哈哈,我承认我第二次作弊了,直接看的答案输入的!

给予用户多次机会

无限机会

一次机会的猜测猜中的几率实在是太低了,这样搞,小红的店都要倒闭了!所以还是多要给用户一些猜测的机会,我们先实现无限机会的版本,直到用户猜测正确才会罢休!

fn main() {
    // ...之前的代码
    println!(":) 来吧!猜猜这个衣服的价格是多少?");

    loop {
        println!("请输入你的猜测:");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("获取你的猜测失败!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("请输入一个有效的数字!");
                return;
            }
        };

        if guess < secret_number {
            println!("你猜的价格太低了!");
        } else if guess > secret_number {
            println!("你猜的价格太高了!");
        } else {
            println!("恭喜你!你猜对了!");
            break;
        }
    }
}

loop的作用上一节我们已经说过,他会无限循环内部的逻辑,猜对的部分我添加了break关键字,主要作用是当用户猜测正确后我们就退出这个loop循环。

我们运行一下看看效果:

这次我可没作弊,挡住了秘密价格进行猜测的!:)

有次数的猜测

终于到了完成小红店长完整需求的时候了,让我们使用一下上一节讲的for循环对区间的遍历方法,改造一下代码:

fn main() {
	// ...之前的代码
    println!(":) 来吧!猜猜这个衣服的价格是多少?");

    for _ in 0..5 {
        println!("请输入你的猜测:");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("获取你的猜测失败!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("请输入一个有效的数字!");
                continue;
            }
        };

        if guess < secret_number {
            println!("你猜的价格太低了!");
        } else if guess > secret_number {
            println!("你猜的价格太高了!");
        } else {
            println!("恭喜你!你猜对了!");
            return;
        }
    }
    println!("很遗憾,你已经用完了5次机会!");
}

我将loop换成的for _ in 0..5,稍微优化了一下流程,如果用户没有输入有效数字的时候也算一次机会,并让用户重新输入。如果答对了直接退出程序。如果用户用完了五次机会,给出提示并退出程序。现在我们运行一下看看效果:

五次机会还是有点难的!这样既有猜测的紧张感,留住客户,也不至于让小红太亏本!

完整代码

下面我们看看完整的代码,我将5这个数字变成了常量MAX_ATTEMPTS,方便后续修改猜测的次数。还删除了秘密价格展示,毕竟开始就知道答案就不好玩了,大家可以注意一下:

use rand::Rng;
use std::io;

const MAX_ATTEMPTS: u32 = 5;

fn main() {
    let mut rng = rand::rng();
    let secret_number = rng.random_range(100..=500);

    println!("===== 欢迎来到猜价格游戏! =====");
    println!(":) 来吧!猜猜这个衣服的价格是多少?");

    for _ in 0..MAX_ATTEMPTS {
        println!("请输入你的猜测:");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("获取你的猜测失败!");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("请输入一个有效的数字!");
                continue;
            }
        };

        if guess < secret_number {
            println!("你猜的价格太低了!");
        } else if guess > secret_number {
            println!("你猜的价格太高了!");
        } else {
            println!("恭喜你!你猜对了!");
            return;
        }
    }
    println!("很遗憾,你已经用完了 {MAX_ATTEMPTS} 次机会!");
}

总结

本小节我们通过实现一个猜价格小游戏项目,一起快速实践了Rust基础语法与核心特性。项目以生成随机数为起点,展示了如何使用rand库的random_range方法,结合Rng特性实现100-500区间的随机定价。在用户交互环节,通过std::io模块实现输入捕获,并运用parse()结合match模式匹配完成类型转换与错误处理,体现了Rust严谨的错误处理机制

核心逻辑通过for循环控制5次猜测机会,利用if条件判断实现价格高低提示,最终通过常量MAX_ATTEMPTS优化代码可维护性。整个项目融合了变量绑定、类型系统、模式匹配、错误处理等Rust核心概念,既巩固了基础语法,也展现了Rust内存安全与表达力强的优势。通过40行左右的代码实践,我们可直观感受Rust在系统编程领域的独特魅力,为后续深入学习打下坚实基础。

本节代码可参照 GitHub

下节预告

下一节我将和大家将直面Rust核心机制——所有权系统与生命周期。揭秘为何所有权能实现零成本内存安全,生命周期如何成为借用检查的关键拼图。通过内存图解与实战案例,带大家一起攻克Rust最硬核的内存管理逻辑,写出真正安全高效的代码!

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧