前言#
昨天我们学了如何新建项目以及构建相关的基础知识,感兴趣的大佬可以看下
今天我们继续跟着文档学习。
猜测游戏#
我们来写一个猜数游戏,简单但是经典~
规则#
系统随机生成一个数,而用户在终端输入一个数,系统会判断并提示高了还是低了,直到这个数被猜到了为止。
创建环境#
我们直接在终端输入
mkdir day2
cd day2
cargo new guessing_game
cd guessing_game可以终端执行cargo run测试下是否正常,这里就不测试了。
分析#
如果要实现这个游戏,我们有以下几个问题需要解决
- 如何生成随机数
- 如何获得用户的输入
- 如何判断
这几个问题我们来一一解决
crate#
不过在实现之前我们需要了解一个概念--crate,中文翻译过来是一个大木箱。。。。怎么说呢,也差不多。。。
我们还是老老实实叫它crate吧。。
那什么是crate呢?我个人理解为这是一个包,可能是一个模块,也可能是多个模块集合。而cargo就是管理这些crate的工具。
所以应该存在以下关系:trait包含于crate包含于cargo。
至于这个trait[3]又是啥,翻译过来叫做“特性”。。。
我们的项目就是一个crate,一个binary crate,可执行的。
安装rand依赖包#
rust自身标准库中并没有可以用来生成随机数的库,所以这需要我们去引入这个依赖。
我们进入Cargo.toml文件中,我们需要引入一个名叫Rand的crate,这个crate是一个library crate,本身不可执行,但可以被调用。

这里0.8.3是^0.8.3的简写,任何高于0.8.3并且低于0.9.0的都符合要求。
然后我们直接终端cargo build。
然后你发现你超时了或者更新的巨慢~doge,那是因为第一次引入依赖会更新crate.io,这里面存放的是所有crate的索引,当你引入某个依赖的时候会告诉cargo如何去安装。 扯远了,就是国外的,基本都很难拉下来,所以我们需要换源。
cargo换源#
首先看下我们的cargo环境路径
echo %PATH%
echo $env:path一般都在C:/User/xxx/.cargo/bin
我们进入到.cargo中,新建一个config.toml文件。
[source.crates-io]
replace-with = "tuna"
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git" 替换为清华的源,然后保存。然后重新cargo build
虽然还是很慢。。。 但是至少不会超时。。。
如果不顶用还有一个可以换
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"生成随机数#
ok,安装完之后我们就来用我们刚引入的包来生成随机数
use rand::Rng;
fn main() {
let rand: isize = rand::thread_rng().gen_range(1..=100);
println!("the rand num is {rand}");
} use表示引用某个库rand::Rng表示使用rand库的Rng,这个Rng是一个trait,这里可以理解为是一个接口,实现了随机数生成的方法,也就是gen_range。::这种写法相信大家应该都知道是做啥用的了,表示获取某个东西里的东西,比如module a中有一个b方法,那么调用b方法就可以是a::b()。1..=100:start..=end,随机数的范围,这里表示1至100之间,包括100。如果不想包括100,可以写1..100。{rand}: 这里花括号中可以直接传入变量,会被替换成对应的值。

生成随机数成功!
获取用户输入#
既然生成随机数成功了,那么第一个问题就解决了。现在来解决第二个问题:获取用户输入。
和c/c++差不多,在node中我们可以调用io模块的stdin来获取,那么在rust其实也是一样的。
rust中使用use std::io因为是标准库中的,所以不需要再去安装依赖。
use rand::Rng;
use std::{io};
fn main() {
let rand = rand::thread_rng().gen_range(1..=100);
println!("the rand num is {rand}");
let guess = String::new();
io::stdin().read_line(guess);
println!("your input is {guess}")
}use std::{io};: 这是一种简写,等价于use std::io,为什么要这么写呢?因为std也就是标准库中不只有io这一个模块,所以如果你导入了std里的多个模块,使用use std::io这种写法就要写很多重复代码。因此官方提供了一种简写方案use x::{a, b::c}。其中a和b同级。stdin:stdin是一个function,指向输入流,调用自然是需要()。read_line: 也是一个function,这个方法是干啥的就不多说了。
如果这个时候你安装了rust-analyzer,你会发现你的read_line这行代码报错了。为什么会报错呢?
可变与不可变、所有权、堆、栈#
实际上上面的报错涉及到一个rust的特点:变量声明的时候默认是不可变的(immutable),那么要怎么做它才能变呢?
很简单,在变量名之前加个mut即可。
修改下代码
let mut guess = String::new();发现保存后还是报错,发现报错是说:期望&mut String, found String。翻译过来也就是期望一个可变的借用的字符串类型,但是传入的却是字符串类型。

这个&又是个啥?
实际上这里涉及到了rust垃圾处理机制,rust中如果变量走出作用域,则会立即被释放掉。
如果变量不带&字符传入方法中,这就表示这个方法中拥有这个变量的所有权。
拥有了所有权是什么意思呢?
我们先来写个demo看下。

借用了已被moved的值:str的值。
当然,并不是所有情况都会这样,比如下面这个


为什么会这样呢?
这又涉及到另一个概念:堆(heap)和栈(stack)。
简单的说stack空间是固定的,每次都是往栈头存和取。而heap则相反,空间是不固定的。
那么stack很适合存放比如指针之类的东西,比如上面这个num变量的值。
而heap适合去存放一些动态的,比如用户输入的字符串,这是不可预测的,所以得动态分配内存。而这个指向内存的指针存放在栈中。
rust中如果某个变量以所有权的形式传入某个方法中,那么就会复制一份栈中的值。当作用域结束时这个值就会出栈,被释放。但是问题又来了,为什么str会报错,而num没有报错呢?
这里又涉及到了一个问题:双重释放问题
由于rust针对堆空间采用的是复制指针的方式,那么就可能出现双重释放问题,也就是复制的指针因为作用域结束被释放掉,导致对应的堆内存也跟着释放掉。而原来指针的作用域结束时也会触发一次堆内存释放,这就存在一个安全性问题:可能第二次释放时这个堆空间已经放了另一个变量的值了,而这次释放导致它无辜躺枪,也没了。
所以针对堆空间双重释放的问题,在以所有权的方式传入另一个作用域时,原来的指针就会被释放掉。这就确保了从创建到释放都只存在一个指针,就很安全。
那么回到我们最开始的问题,什么是所有权。也就是一个值只能属于一个变量。
stack也就是栈是通过copy来实现的,所以也是符合的。比如let x = 1; let y = x;这其中copy了一份x的值给y,变成了let x = 1; let y = 1;,也是符合一个值只能拥有一个变量的限制。
需要存放到heap也就是堆上的则是通过move指针的方式来实现,move完立即释放掉原来的变量,也符合了一个值只属于一个变量。
而&符号写上则表示这是一个引用,并非move。相当于创建了一个新指针指向这个变量的那个指针,这样原来的那个变量就不会被释放掉。
修改获取输入的代码#
既然知道问题是怎么回事了,那么我们就来改下我们的代码
use rand::Rng;
use std::{io};
fn main() {
let rand = rand::thread_rng().gen_range(1..=100);
println!("the rand num is {rand}");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("something error");
println!("your input is {guess}")
}
运行正常。
比较输入和随机数#
第二个问题也解决了,第三个问题:如何判断。
我这里就直接贴下代码
use rand::Rng;
use std::{io, cmp::Ordering};
fn main() {
let rand = rand::thread_rng().gen_range(1..=100);
println!("the rand num is {rand}");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("something error");
println!("your input is {guess}");
match guess.cmp(&mut rand) {
Ordering::Greater => println!("is larger"),
Ordering::Less => println!("is smaller"),
Ordering::Equal => {
println!("you win!");
}
}
}match:表示匹配表达式,写法是match 匹配表达式 { 匹配内容表达式 }cmp:一个通用的模块,暴露出接口,可被实现。Ordering:cmp模块中的一个枚举

Ordering::Greater: 表示关联的是这个Greater。=> {xxx}:这个是匹配到要执行的语句。
执行下cargo run
但是又报错了

这是因为我们的rand随机数是一个i32整数类型。而我们从输入中拿到的是一个String类型,所以就报错了。
我们来转换下
use rand::Rng;
use std::{io, cmp::Ordering};
fn main() {
let mut rand = rand::thread_rng().gen_range(1..=100);
println!("the rand num is {rand}");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("something error");
let guess: u32 = guess.trim().parse().expect("parse error");
println!("your input is {guess}");
match guess.cmp(&mut rand) {
Ordering::Greater => println!("is larger"),
Ordering::Less => println!("is smaller"),
Ordering::Equal => {
println!("you win!");
}
}
} 其中let guess: u32 = guess.trim().parse().expect("parse error");是调用字符串自身的trim去除前后空格然后做parse处理,当然由于用户可能输入一个非数字,所以这里parse可能会有问题,得加上expect输出错误。
现在我们来重新试下cargo run

ok这就匹配成功啦~
总结#
今天(实际上可能是好几天。。。没办法要上班)我们自己做了一个猜测游戏,也知道了一些rust的特性,比如如何处理内存等。不过这个代码还有调整空间,明天我们来优化下。
另外挺多地方没有贴上参考链接,主要是太多了。。。后面有空我再补上。
觉得有用的话麻烦点个赞,谢谢!
参考#
- ^rust-demo-guessing-game https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html#programming-a-guessing-game
- ^crate https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html#using-a-crate-to-get-more-functionality
- ^trait https://doc.rust-lang.org/stable/book/ch10-02-traits.html#traits-defining-shared-behavior
- ^cargo replace registry https://doc.rust-lang.org/cargo/reference/source-replacement.html#source-replacement
发布于 2022-11-26 00:06・IP 属地广东
