目录
本章节以一个精心设计的实际工程项目,先来初次尝试一下rust语言编程和工程应用的实践。该项目是一个:猜数游戏,主要工作流程如下:
1)程序生成1~100之间的一个随机整数;2)用户输入猜测的数字;3)程序输出猜测的数字大于或小于生成数;4)用户猜测正确,结束运行,用户猜测错误,可重复2、3步骤。
- # cargo new guessing_game && cd guessing_game
- Created binary (application) `guessing_game` package
- # cargo run
- Compiling guessing_game v0.1.0 (/doc/jiangxiaoqing/rust/chapter2/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 0.31s
- Running `target/debug/guessing_game`
- Hello, world!
- # ls
- Cargo.lock Cargo.toml src target
- # tree
- .
- ├── Cargo.lock
- ├── Cargo.toml
- ├── src
- │ └── main.rs
- └── target
- ├── CACHEDIR.TAG
- └── debug
- ├── build
- ├── deps
- │ ├── guessing_game-2e7ea747ef88670f
- │ └── guessing_game-2e7ea747ef88670f.d
- ├── examples
- ├── guessing_game
- ├── guessing_game.d
- └── incremental
- └── guessing_game-1vpxsh8m6pzam
- ├── s-gfeqim8j6x-ein778-3lplmqmartfnp
- │ ├── 1ez1ge870eb6gq7l.o
- │ ├── 1sx3ehn655us4o1e.o
- │ ├── 1u60bgs93g3f9qgi.o
- │ ├── 2zd4w7ck950yptxq.o
- │ ├── 4ugvrlcdoppn14n.o
- │ ├── 57xo5z3lc17i2l94.o
- │ ├── dep-graph.bin
- │ ├── query-cache.bin
- │ └── work-products.bin
- └── s-gfeqim8j6x-ein778.lock
-
- 9 directories, 18 files
生成一个目标项目,可以直接先运行一下,后面步骤编辑main.rs来完成该项目。
首先,我们来处理用户输入的交互部分:允许用户输入一个数字
- use std::io;
-
- fn main() {
- println!("Guess the number!");
-
- println!("Please input your guess.");
-
- let mut guess = String::new();
-
- io::stdin()
- .read_line(&mut guess)
- .expect("Failed to read line");
-
- println!("You guessed: {}", guess);
- }
以上代码使用了rust标准库(std)中的io库,默认情况下,rust在标准库中的某些项能被自动载入。而非自动载入的需要用use语句来引入。如代码,io库中的某些功能能让我们处理用户输入。
let可以定义新的变量,并绑定到后面的字符串对象。这里的mut标识该变量是允许修改的,后面第3章节会解释。String::new函数会创建并实例化一个空的字符串。
std::io::stdin()函数,返回一个std::io::Stdin的对象实例,代表一个到terminal终端标准输入的句柄。后面的.read_line()函数接收用户的一行输入,并append到字符串对象guess中。& 符号指示参数使用引用,而不需要拷贝参数。引用 是rust语言中的一个比较复杂的特性,默认情况下是不可变 的,因此这里使用了&mut 来使其是可修改的。
readline()函数返回一个io::Result() 的类型对象。rust标准库中包含一系列的Result命名的类型。io中的Result是一个枚举类型,包含固定数量变量的一个集合。枚举类型通常与match 一起使用,match是一个条件匹配。这里的Result可以取值Ok 和Err 。
Ok 表示当前操作是成功的,Ok内部存储了生成的值;Err 表示当前操作失败,Err内部存储了失败的信息。
io::Result对象也有方法,例如expect方法。如果io::Result是一个Err对象,则程序crash,并打印一条expect调用参数的字符串。如果不调用expect()方法,则编译会报一个warning,警告未处理可能发生的错误。
使用一个外部库rand crate,来生成伪随机数。可以多次玩猜数游戏。
cargo管理了线上(crates.io)的库注册,以及本地工程的crates依赖,首先我们要添加一个rand依赖到cargo的配置文件Cargo.toml中。

在配置文件的dependencies section中添加依赖的crate名和版本号。这里的0.8.5 是一个0.8.*最低版本以及不高于0.9.0版本的语义,而非特定版本,即:高于0.8.5版本小于0.9版本的都可以。cargo会从线上拉取一个最新的满足版本要求的rand下来到${home}/.cargo目录中。在这个过程中,rand自身依赖的crates同样被递归拉下来了。 这种工程的库和版本依赖管理,对于rust工程开发非常重要。
- # cargo build
- Updating crates.io index
- Downloaded ppv-lite86 v0.2.17
- Downloaded getrandom v0.2.8
- Downloaded rand_chacha v0.3.1
- Downloaded rand v0.8.5
- Downloaded rand_core v0.6.4
- Downloaded cfg-if v1.0.0
- Downloaded libc v0.2.137
- Downloaded 7 crates (791.9 KB) in 0.97s
- Compiling libc v0.2.137
- Compiling cfg-if v1.0.0
- Compiling ppv-lite86 v0.2.17
- Compiling getrandom v0.2.8
- Compiling rand_core v0.6.4
- Compiling rand_chacha v0.3.1
- Compiling rand v0.8.5
- Compiling guessing_game v0.1.0 (/doc/jiangxiaoqing/rust/chapter2/guessing_game)
- Finished dev [unoptimized + debuginfo] target(s) in 4.55s
- # ls ~/.cargo/registry/src/github.com-1ecc6299db9ec823/
- cfg-if-1.0.0 getrandom-0.2.8 libc-0.2.137 ppv-lite86-0.2.17 rand-0.8.5 rand_chacha-0.3.1 rand_core-0.6.4
Cargo.lock文件包含了什么?
留意,在项目根目录有一个Cargo.lock文件,该文件的作用是什么呢?
Cargo.lock本质上一个crates依赖的本地缓存,会在第一次cargo build时产生。 假设首次build时,从crate.io拉到了0.8.5版本的rand,而过了一段时间之后,0.8.6版本在线上被发布,但该版本无法在我们的guessing_game中运行。cargo本身总是拉取符合条件的最高版本,但存在Cargo.lock时,则优先根据历史缓存中的版本来拉取和构建。这保证了我们工程总是可以重复构建成功的,即使线上registry中发布了符合我们版本期望但却导致我们工程无法编译的crates版本。那么,如何更新到线上最新版本呢?通过执行cargo update拉拉取线上符合条件的最新版本的各个crates。
我们将在14章节,重点介绍Cargo生态。
- use std::io;
- use rand::Rng;
-
- fn main() {
- println!("Guest the number:");
- let secret_number = rand::thread_rng().gen_range(1..101);
- println!("The secret number is: {}", secret_number);
- println!("Please input your guesss.");
- let mut guess = String::new();
-
- io::stdin().read_line(&mut guess).expect("Failed to read line");
-
- println!("You guessed: {}", guess);
- }
rand::Rng中的Rng 是一个trait ,在第10章将介绍什么是trait 。Rng中定义了生成随机数的方法。
thread_rng函数返回一个随机数发生器对象,只跟当前线程相关的随机数发生器,使用了操作系统决定的随机种子。gen_range()函数利用随机数发生器,生成指定参数范围内的伪随机数。如上:1..101表示1到100之间的数字。
range表达式start..end的范围时[start, end),即不包括end。也可以写作,start..=end,表示[start, end]。
小技巧:
使用cargo doc --open会编译出本地工程以及依赖的所有crates的相关使用文档,就可以查询特定crate中函数的使用手册。也可以在crate.io线上搜索相关库的crate文档手册。
有了生成的随机数和用户输入的数字,就可以进行比对。
- use std::io;
- use rand::Rng;
- use std::cmp::Ordering;
-
- fn main() {
- println!("Guest the number:");
- let secret_number = rand::thread_rng().gen_range(1..101);
- println!("The secret number is: {}", secret_number);
- println!("Please input your guesss.");
- let mut guess = String::new();
-
- io::stdin().read_line(&mut guess).expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => println!("You win!"),
- }
- println!("You guessed: {}", guess);
- }
该类型是标准库提供的一个枚举类型,可以取值Less、Greater、Equal 。cmp 函数可以比较任意两个“可比较”的类型值,并返回一个std::cmp::Ordering类型对象。
类似C语言中的switch...case语句,match表达式包含多个arms(case语句?)。每个arm是一个匹配模式,rust会依次尝试匹配每一个arm。第6和第18章节将详细介绍。
由于输入的捕获是一种字符串类型,无法与生成伪随机数的数字类型进行匹配比对。rust是强类型的编程语言。生成的随机数默认是一个i32类型,(32位的有符号整型数字)。
第2个定义的guest将字符串的guest给覆盖了,trim()函数是字符串guest对象的一个方法,将我们输入的字符串前后的空格给过滤掉,然后用parse()尝试转换成u32类型的数字。
到此,我们可以进行一次猜测,接下来使用rust的循环语句loop运行用户进行多次猜测,直到猜成功。
- use std::io;
- use rand::Rng;
- use std::cmp::Ordering;
-
- fn main() {
- println!("Guest the number:");
- let secret_number = rand::thread_rng().gen_range(1..101);
- // println!("The secret number is: {}", secret_number);
- loop {
- println!("Please input your guesss.");
- let mut guess = String::new();
-
- io::stdin().read_line(&mut guess).expect("Failed to read line");
-
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
-
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- //println!("You guessed: {}", guess);
- }
- }
上述代码,用户无限猜测,直到猜数成功,或者输入非法(非数字)。parse()失败,程序会直接crash退出,并输出错误提示“Please type a number!”。
到目前为止,用户输入非法(非数字)时会导致程序直接crash(输出一个错误消息)。为了不使程序崩溃,更友好提示用户重新输入,做如下改造:忽略用户的非法输入,并运行再次输入。
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
parse()返回的是一个Result类型对象,而Result对象是一个具有Ok和Err两个枚举值的枚举类型。这样,我们同样可以用match表达式(match是表达式expression,而非语句statement) ,来匹配并区分处理。
如果parse()成功将字符串转换为一个数字,将返回一个包含结果数字的Ok枚举值,则直接返回该数字;如果失败,返回一个包含错误消息的Err枚举值,Err(_)中下划线的语义是匹配任意值,即匹配所有类型的Err,不论内部信息如何。continue将返回到loop循环的下一轮循环开始处运行。 因此,上述代码,忽略了parse()遇到的所有错误。
最终版本代码如下:
- use std::io;
- use rand::Rng;
- use std::cmp::Ordering;
-
- fn main() {
- println!("Guest the number:");
- let secret_number = rand::thread_rng().gen_range(1..101);
- // println!("The secret number is: {}", secret_number);
- loop {
- println!("Please input your guesss.");
- let mut guess = String::new();
-
- io::stdin().read_line(&mut guess).expect("Failed to read line");
-
- // let guess: u32 = guess.trim().parse().expect("Please type a number!");
- let guess: u32 = match guess.trim().parse() {
- Ok(num) => num,
- Err(_) => continue,
- };
- match guess.cmp(&secret_number) {
- Ordering::Less => println!("Too small!"),
- Ordering::Greater => println!("Too big!"),
- Ordering::Equal => {
- println!("You win!");
- break;
- }
- }
- //println!("You guessed: {}", guess);
- }
- }
运行测试:
- # cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.00s
- Running `target/debug/guessing_game`
- Guest the number:
- Please input your guesss.
- 60
- Too small!
- Please input your guesss.
- sd
- Please input your guesss.
- 90
- Too big!
- Please input your guesss.
- 75
- Too small!
- Please input your guesss.
- 83
- Too big!
- Please input your guesss.
- 78
- You win!
关于作者:
犇叔,浙江大学计算机科学与技术专业,研究生毕业,而立有余。先后在华为、阿里巴巴和字节跳动,从事技术研发工作,资深研发专家。主要研究领域包括虚拟化、分布式技术和存储系统(包括CPU与计算、GPU异构计算、分布式块存储、分布式数据库等领域)、高性能RDMA网络协议和数据中心应用、Linux内核等方向。
专业方向爱好:数学、科学技术应用
关注犇叔,期望为您带来更多科研领域的知识和产业应用。
内容坚持原创,坚持干货有料。坚持长期创作,关注犇叔不迷路