入门
Table of Contents
Hello World
编写rust程序
fn main() { println!("Hello, world!"); }
编译并运行文件
$ rustc main.rs $ ./main Hello, world!
分析程序
fn main() { }
这几行定义了一个 Rust 函数。 main 函数是一个 特殊的 函数:
- 在可执行的 Rust 程序中,它总是最先运行的代码
- 第一行代码声明了一个叫做 main 的函数
- 它 没有参数 也 没有返回值
- 如果有参数的话,它们的名称应该出现在小括号中 ()
还须注意,函数体被包裹在花括号中,{} Rust 要求所有函数体都要用花括号包裹起来 一般来说,将左花括号与函数声明置于同一行并以空格分隔,是良好的代码风格
在 main () 函数中是如下代码:
println!("Hello, world!");
这行代码完成这个简单程序的所有工作:在屏幕上打印文本。这里有四个重要的细节需要注意:
- Rust 的缩进风格使用 4 个空格 ,而不是 1 个制表符
- println! 调用了一个 Rust 宏 。如果是调用函数,则应输入 println ( 没有! )
- "Hello, world!" 是一个字符串。把这个字符串作为一个参数传递给 println!,字符串将被打印到屏幕上
- 该行以 分号结尾 ; ,这代表一个表达式的结束和下一个表达式的开始。大部分 Rust 代码行以分号结尾
编译和运行是独立的
Rust 是一种 预编译静态 类型语言,这意味着可以编译程序,并将 可执行文件 送给其他人,他们甚至不需要安装 Rust 就可以运行
如果你给他人一个 .rb、.py 或 .js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM) 不过在这些语言中,只需要一句命令就可以编译和运行程序 这一切都是语言设计上的权衡取舍
Hello Cargo
仅仅使用 rustc 编译简单程序是没问题的,不过随着项目的增长,可能需要管理你项目的方方面面,并让代码易于分享 接下来,要介绍一个叫做 Cargo 的工具,它会帮助编写真实世界中的 Rust 程序
Cargo 是 Rust 的 构建系统 和 包管理器 ,它可以为你处理很多任务,比如 构建代码 、 下载依赖库 并 编译这些库
$ cargo --version cargo 1.40.0 (bc8e4c8be 2019-11-22)
如果看到了版本号,说明已安装
使用 Cargo 创建项目
$ cargo new hello_cargo $ cd hello_cargo
- 第一行命令 新建 了名为 hello_cargo 的目录。将项目命名为 hello_cargo,同时 Cargo 在一个同名目录中创建项目文件
- 进入 hello_cargo 目录并列出文件。将会看到 Cargo 生成了两个文件和一个目录:
- 一个 Cargo.toml 文件
- 一个 src 目录
- 位于 src 目录中的 main.rs 文件
Cargo.toml
[package] name = "hello_cargo" version = "0.1.0" authors = ["i514692 <klose.wu@sap.com>"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
这个文件使用 TOML 格式,这是 Cargo 配置文件的格式:
- 第一行 [package] ,是一个片段标题,表明下面的语句用来 配置一个包
- 随着在这个文件增加更多的信息,还将增加其他片段
- 接下来的四行设置了 Cargo 编译程序所需的配置:
- 项目的名称
- 版本
- 作者
- 要使用的 Rust 版本
Cargo 从环境中获取你的名字和 email 信息,所以如果这些信息不正确,请修改并保存此文件
- 最后一行 [dependencies] ,是罗列 项目依赖 的片段的开始
在 Rust 中,代码包被称为 crates 这个项目并不需要其他的 crate
main.rs
fn main() { println!("Hello, world!"); }
Cargo 自动生成了一个 “Hello, world!” 程序
Cargo 期望源文件存放在 src 目录中。项目根目录只存放 README、license 信息、配置文件和其他跟代码无关的文件 使用 Cargo 帮助你保持项目干净整洁,一切井井有条。
构建并运行
$ cargo build
Compiling hello_cargo v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.71s
这个命令会 创建一个可执行文件 target/debug/hello_cargo 可以通过这个命令运行可执行文件:
$ ./target/debug/hello_cargo Hello, world!
如果一切顺利,终端上应该会打印出 Hello, world!
首次运行 cargo build 时,也会使 Cargo 在项目根目录创建一个新文件:Cargo.lock。这个文件记录项目依赖的实际版本 这个项目并没有依赖,所以其内容比较少。你自己永远也不需要碰这个文件,让 Cargo 处理它就行了。
也可以使用 cargo run 在一个命令中 同时编译并运行 生成的可执行文件:
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.04s Running `target/debug/hello_cargo` Hello, world!
注意这一次并没有出现表明 Cargo 正在编译 hello_cargo 的输出 Cargo 发现文件并没有被改变,就直接运行了二进制文件 如果修改了源文件的话,Cargo 会在运行之前重新构建项目
Cargo 还提供了一个叫 cargo check 的命令。该命令 快速检查代码确保其可以编译 ,但并不产生可执行文件:
$ cargo check
Checking hello_cargo v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.22s
通常 cargo check 要比 cargo build 快得多,因为它省略了生成可执行文件的步骤 如果你在编写代码时持续的进行检查,cargo check 会加速开发! 为此很多 Rustaceans 编写代码时定期运行 cargo check 确保它们可以编译。当准备好使用可执行文件时才运行 cargo build
回顾下已学习的 Cargo 内容:
- 可以使用 cargo build 或 cargo check 构建项目
- 可以使用 cargo run 一步构建并运行项目
- 有别于将构建结果放在与源码相同的目录,Cargo 会将其放到 target/debug 目录
发布构建
当项目最终准备好发布时,可以使用 cargo build –release 来 优化编译 项目。这会在 target/release 而不是 target/debug 下生成可执行文件
这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间 这也就是为什么会有两种不同的配置: 一种是为了开发,你需要经常快速重新构建 另一种是为用户构建最终程序,它们不会经常重新构建,并且希望程序运行得越快越好 如果你在测试代码的运行时间,请确保运行 cargo build --release 并使用 target/release 下的可执行文件进行测试
把 Cargo 当成习惯
对于简单项目,Cargo 并不比 rustc 提供了更多的优势,不过随着开发的深入,终将证明其价值 对于拥有多个 crate 的复杂项目,交给 Cargo 来协调构建将简单的多
猜猜看游戏
创建一个新项目
$ cargo new guessing_game $ cd guessing_game
处理一次猜测
猜猜看程序的第一部分请求和处理用户输入,并检查输入是否符合预期的格式
首先,允许玩家输入猜测。在 src/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); }
导入包
为了获取用户输入并打印结果作为输出,需要将 io ( 输入/输出 )库引入当前作用域。io 库来自于 标准库 (也被称为 std )
use std::io;
默认情况下,Rust 将 prelude 模块中少量的类型引入到每个程序的作用域中。如果需要的类型不在 prelude 中,你必须使用 use 语句显式地将其引入作用域
std::io 库提供很多有用的功能,包括接收用户输入的功能
定义变量
创建一个储存用户输入的地方:
let mut guess = String::new();
这是一个 let 语句,用来 创建变量 。这里是另外一个例子:
let foo = 5; // 不可变的
在 Rust 中,变量 默认是不可变 的,程序里的 mut 用来定义 可变的 变量
注意:// 语法开始一个注释,持续到行尾。Rust 忽略注释中的所有内容
new函数
现在已经知道了 let mut guess 会引入一个叫做 guess 的可变变量。等号(=) 的右边是 guess 所绑定的值,它是 String::new 的结果,这个函数会返回一个 String 的新实例
String 是一个标准库提供的字符串类型,它是 UTF-8 编码的可增长文本块
::new 那一行的 :: 语法表明 new 是 String 类型的一个 关联函数 ,关联函数是针对类型实现的
一些语言中把它称为 静态方法 在这个例子中 new 函数是 String,而不是 String 的某个特定实例 new 函数创建了一个新的空字符串,很多类型上有 new 函数,因为它是创建类型实例的惯用函数名
引用
在程序的第一行使用 use std::io ; 从标准库中引入了输入/输出功能。现在调用 io 的关联函数 stdin :
io::stdin().read_line(&mut guess) .expect("Failed to read line");
stdin 函数返回一个 std::io::Stdin 的实例,这代表 终端标准输入句柄的类型
如果程序的开头没有 use std::io 这一行,可以把函数调用写成 std::io::stdin
.read_line(&mut guess) : 调用 read_line 方法从 标准输入句柄 获取 用户输入 ,向 read_line() 传递了一个 参数 &mut guess
- 无论用户在标准输入中键入什么内容,都将其存入一个字符串中,因此它需要字符串作为参数
- 这个字符串参数应该是 可变的 ,以便 read_line 将用户输入附加上去
引用是一个复杂的特性,Rust 的一个主要优势就是安全而简单的操纵引用 当然完成当前程序并不需要了解如此多细节
& 表示这个参数是一个 引用 ,它允许 多处代码访问同一处数据 ,而 无需在内存中多次拷贝
- 它像变量一样,*默认是不可变的*
- 所以要写成 &mut guess 来使其可变,而不是 &guess
使用 Result 类型来处理潜在的错误
read_line 将用户输入附加到传递给它的字符串中,不过它也 返回一个值 :在这个例子中是 io::Result
Rust 标准库中有很多叫做 Result 的类型 一个 Result 泛型以及对应子模块的特定版本,比如 io::Result
Result 类型是 枚举 ,通常也写作 enums 。枚举类型 持有固定集合的值 ,这些值被称为枚举的 成员 。Result 的成员是 Ok 和 Err
- Ok 成员表示操作成功,内部包含成功时产生的值
- Err 成员则意味着操作失败,并且包含失败的前因后果
这些 Result 类型的作用是编码错误处理信息
Result 类型的值,像其他类型一样,拥有定义于其上的方法。 io::Result 的实例拥有 expect 方法:
- 如果 io::Result 实例的值是 Err,expect 会导致 程序崩溃 ,并 显示当做参数传递给 expect 的信息
- 如果 read_line 方法返回 Err,则可能是来源于 底层操作系统错误的结果
- 如果 io::Result 实例的值是 Ok,expect 会 获取 Ok 中的值 并 原样返回
- 这里这个值是 用户输入到标准输入中的字节数
如果不调用 expect,程序也能编译,不过会出现一个警告: $ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) warning: unused `std::result::Result` which must be used --> src/main.rs:10:5 | 10 | io::stdin().read_line(&mut guess); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: #[warn(unused_must_use)] on by default Rust 警告没有使用 read_line 的返回值 Result,说明有一个可能的错误没有处理 消除警告的正确做法是实际编写错误处理代码,不过由于就是希望程序在出现问题时立即崩溃,所以直接使用 expect。
使用 println! 占位符打印值
println!("You guessed: {}", guess);
这行代码打印存储用户输入的字符串。第一个参数是 格式化字符串 :
- 里面的 {} 是 预留在特定位置的占位符
- 使用 {} 也可以打印多个值:第一对 {} 使用格式化字符串之后的第一个值,第二对则使用第二个值,依此类推
调用一次 println! 打印多个值看起来像这样:
#![allow(unused_variables)] fn main() { let x = 5; let y = 10; println!("x = {} and y = {}", x, y); }
这行代码会打印出 x = 5 and y = 10
测试第一部分代码
使用 cargo run 运行:
$ cargo run Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 0.76s Running `target/debug/guessing_game` Guess the number! Please input your guess. 8 You guessed: 8
生成一个秘密数字
接下来,需要生成一个秘密数字,好让用户来猜
秘密数字应该每次都不同,这样重复玩才不会乏味 范围应该在 1 到 100 之间,这样才不会太困难
Rust 标准库中尚未包含随机数功能。然而,Rust 团队还是提供了一个 rand crate
使用 crate 来增加更多功能
crate 是一个 Rust 代码包 :
- 正在构建的项目是一个 二进制 crate ,它 生成一个可执行文件
- rand crate 是一个 库 crate ,可以 包含任意能被其他程序使用的代码
Cargo 对外部 crate 的运用是其真正闪光的地方
使用 rand 编写代码之前,需要修改 Cargo.toml 文件, 引入一个 rand 依赖 。现在打开这个文件并在底部的 [dependencies] 片段标题之下添加:
[dependencies] rand = "0.5.5"
在 Cargo.toml 文件中,标题以及之后的内容属同一个片段,直到遇到下一个标题才开始新的片段
[dependencies] 片段告诉 Cargo 本项目 依赖了哪些外部 crate 及其版本 :这里使用语义化版本 0.5.5 来指定 rand crate
Cargo 理解语义化版本(有时也称为 SemVer),这是一种定义版本号的标准 0.5.5 事实上是 ^0.5.5 的简写,它表示 “任何与 0.5.5 版本公有 API 相兼容的版本”
现在,不修改任何代码,构建项目:
$ cargo build
Updating crates.io index
Downloaded rand v0.5.6
Downloaded rand_core v0.3.1
Downloaded rand_core v0.4.2
Downloaded libc v0.2.66
Compiling libc v0.2.66
Compiling rand_core v0.4.2
Compiling rand_core v0.3.1
Compiling rand v0.5.6
Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 30.78s
可能会出现不同的版本号(多亏了语义化版本,它们与代码是兼容的!),同时显示顺序也可能会有所不同
Cargo 从 registry 上获取所有包的最新版本信息,这是一份来自 Crates.io 的数据拷贝 Crates.io 是 Rust 生态环境中的开发者们向他人贡献 Rust 开源项目的地方
在更新完 registry 后,Cargo 检查 [dependencies] 片段并下载缺失的 crate
本例中,虽然只声明了 rand 一个依赖,然而 Cargo 还是额外获取了 libc 和 rand_core 的拷贝,因为 rand 依赖 libc 来正常工作 下载完成后,Rust 编译依赖,然后使用这些依赖编译项目
如果不做任何修改,立刻再次运行 cargo build,则不会看到任何除了 Finished 行之外的输出。Cargo 知道它已经下载并编译了依赖,同时 Cargo.toml 文件也没有变动。Cargo 还知道代码也没有任何修改,所以它不会重新编译代码。因为无事可做,它简单的退出了。如果打开 src/main.rs 文件,做一些无关紧要的修改,保存并再次构建,则会出现两行输出:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
这一行表示 Cargo 只针对 src/main.rs 文件的微小修改而更新构建。依赖没有变化,所以 Cargo 知道它可以复用已经为此下载并编译的代码。它只是重新构建了部分(项目)代码
Cargo.lock 文件确保构建是可重现的
Cargo 有一个机制来 确保任何人在任何时候重新构建代码,都会产生相同的结果 :Cargo 只会使用你指定的依赖版本,除非你又手动指定了别的
例如,如果下周 rand crate 的 0.5.7 版本出来了,它修复了一个重要的 bug,同时也含有一个会破坏代码运行的缺陷,这时会发生什么呢?
Cargo.lock 文件。在第一次运行 cargo build 时创建,并放在 guessing_game 目录。当第一次构建项目时,Cargo 计算出所有符合要求的依赖版本并写入 Cargo.lock 文件。当将来构建项目时,Cargo 会发现 Cargo.lock 已存在并使用其中指定的版本,而不是再次计算所有的版本。这使得拥有了一个自动化的可重现的构建
换句话说,项目会持续使用 0.5.6 直到你显式升级,多亏有了 Cargo.lock 文件
更新 crate 到一个新版本
当 确实 需要升级 crate 时,Cargo 提供了另一个命令, update :它会忽略 Cargo.lock 文件,并计算出所有符合 Cargo.toml 声明的最新版本。如果成功了,Cargo 会把这些版本写入 Cargo.lock 文件
不过 Cargo 默认只会寻找大于 0.5.6 而小于 0.6.0 的版本
如果 rand crate 发布了两个新版本,0.5.7 和 0.6.0,在运行 cargo update 时会出现如下内容:
$ cargo update Updating crates.io index Updating rand v0.5.6 -> v0.5.7
如果想要使用 0.6.0 版本的 rand 或是任何 0.6.x 系列的版本,必须像这样手动更新 Cargo.toml 文件:
[dependencies] rand = "0.6.0"
下一次运行 cargo build 时,Cargo 会从 registry 更新可用的 crate,并根据指定的新版本重新计算
生成一个随机数字
使用 rand,更新 src/main.rs :
use std::io; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is: {}", secret_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); }
新增了一行 use rand::Rng :Rng 是一个 trait ,它定义了随机数生成器应实现的方法,想使用这些方法的话,此 trait 必须在作用域中
注意:不可能凭空就知道应该 use 哪个 trait 以及该从 crate 中调用哪个方法。crate 的使用说明位于其文档中 Cargo 有一个很棒的功能是:运行 cargo doc --open 命令来构建所有本地依赖提供的文档,并在浏览器中打开 例如,假设你对 rand crate 中的其他功能感兴趣,你可以运行 cargo doc --open 并点击左侧导航栏中的 rand
接下来,在中间还新增加了两行:
- rand::thread_rng 函数 提供实际使用的随机数生成器 :它位于 当前执行线程的本地环境 中,并从 操作系统 获取 seed
- 调用随机数生成器的 gen_range 方法。这个方法由刚才引入到作用域的 Rng::trait 定义:
- 获取两个数字作为参数,并生成一个范围在两者之间的随机数
- 它包含下限但不包含上限,所以需要指定 1 和 101 来请求一个 1 和 100 之间的数
- 调用随机数生成器的 gen_range 方法。这个方法由刚才引入到作用域的 Rng::trait 定义:
- 第二行就是打印出这个随机数字
$ cargo run Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 0.95s Running `target/debug/guessing_game` Guess the number! The secret number is: 24 Please input your guess. 8 You guessed: 8
比较猜测的数字和秘密数字
现在有了用户输入和一个随机数,可以比较它们:
use std::io; use std::cmp::Ordering; use rand::Rng; fn main() { // ---snip--- println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
新代码的第一行是另一个 use,从标准库引入了一个叫做 std::cmp::Ordering 的类型。同 Result 一样, Ordering 也是一个 枚举 ,不过它的成员是 Less 、 Greater+ 和 Equal 。这是比较两个值时可能出现的三种结果
底部的五行新代码使用了 Ordering 类型:
- cmp 方法用来比较两个值并可以在任何可比较的值上调用:
- 它获取一个 被比较值的引用 :这里是把 guess 与 secret_number 做比较
- 返回一个刚才通过 use 引入作用域的 Ordering 枚举的成员
- 使用一个 match 表达式 ,根据对 guess 和 secret_number 调用 cmp 返回的 Ordering 成员来决定接下来做什么
模式匹配
一个 match 表达式 由 分支 构成
- 一个分支包含一个 模式 和 表达式开头的值与分支模式 相 匹配 时应该 执行的代码
- Rust 获取提供给 match 的值并挨个检查每个分支的模式
- match 结构和模式是 Rust 中强大的功能,它体现了代码可能遇到的多种情形,并确保没有遗漏处理
假设用户猜了 50,这时随机生成的秘密数字是 38,比较 50 与 38 时,因为 50 比 38 要大,cmp 方法会返回 Ordering::Greater Ordering::Greater 是 match 表达式得到的值: 1. 它检查第一个分支的模式,Ordering::Less 与 Ordering::Greater并不匹配,所以它忽略了这个分支的代码并来到下一个分支 2. 下一个分支的模式是 Ordering::Greater,正确匹配!这个分支关联的代码被执行,在屏幕打印出 Too big! match 表达式就此终止,因为该场景下没有检查最后一个分支的必要
静态强类型
然而上面的代码却不能通过编译:
$ cargo build Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) error[E0308]: mismatched types --> src/main.rs:21:21 | 21 | match guess.cmp(&secret_number) { | ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer | = note: expected type `&std::string::String` found type `&{integer}` error: aborting due to previous error For more information about this error, try `rustc --explain E0308`. error: could not compile `guessing_game`. To learn more, run the command again with --verbose.
错误的核心表明这里有 不匹配的类型
Rust 有一个静态强类型系统,同时也有类型推断 当写出 let guess = String::new() 时,Rust 推断出 guess 应该是 String 类型,并不需要写出类型 另一方面,secret_number,是数字类型。几个数字类型拥有 1 到 100 之间的值:32 位数字 i32;32 位无符号数字 u32;64 位数字 i64 等等 Rust 默认使用 i32,所以它是 secret_number 的类型,除非增加类型信息,或任何能让 Rust 推断出不同数值类型的信息 这里错误的原因在于 Rust 不会比较字符串类型和数字类型
必须把从 输入中读取到的String 转换 为一个真正的 数字类型 ,才好与秘密数字进行比较。这可以通过在 main 函数体中增加如下两行代码来实现:
// --snip-- 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!"); println!("You guessed: {}", guess); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
这里创建了一个叫做 guess 的变量
不过等等,不是已经有了一个叫做 guess 的变量了吗?
确实如此,不过 Rust 允许用一个新值来 隐藏 guess 之前的值
这个功能常用在需要转换值类型之类的场景。它允许我们复用 guess 变量的名字,而不是被迫创建两个不同变量,诸如 guess_str 和 guess 之类
将 guess 绑定到 guess.trim().parse() 表达式上:
- 表达式中的 guess 是包含输入的 原始 String 类型
- String 实例的 trim 方法会 去除字符串开头和结尾的空白字符
u32 只能由数字字符转换,不过用户必须输入 enter 键才能让 read_line 返回,然而用户按下 enter 键时,会在字符串中增加一个换行符 例如,用户输入 5 并按下 enter,guess 看起来像这样:5\n。\n 代表 “换行”,回车键。trim 方法消除 \n,只留下 5
- 字符串的 parse 方法 将 字符串解析成数字 :
- 因为这个方法可以解析多种数字类型,因此 需要告诉 Rust 具体的数字类型 ,这里通过 let guess: u32 指定
- guess 后面的冒号 : 告诉 Rust 指定了 变量的类型
- 因为这个方法可以解析多种数字类型,因此 需要告诉 Rust 具体的数字类型 ,这里通过 let guess: u32 指定
Rust 有一些内建的数字类型;u32 是一个无符号的 32 位整型 对于不大的正整数来说,它是不错的类型 另外,程序中的 u32 注解以及与 secret_number 的比较,意味着 Rust 会推断出 secret_number 也是 u32 类型 现在可以使用相同类型比较两个值了
- parse 调用很容易产生错误:
- 例如,字符串中包含 A👍% ,就无法将其转换为一个数字
- parse 方法返回一个 Result 类型,再次用 expect 方法处理即可:
- 如果 parse 不能从字符串生成一个数字,返回一个 Result 的 Err 成员时,expect 会使游戏崩溃并打印附带的信息
- 如果 parse 成功地将字符串转换为一个数字,它会返回 Result 的 Ok 成员,然后 expect 会返回 Ok 值中的数字
现在运行程序:
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.08s Running `target/debug/guessing_game` Guess the number! The secret number is: 83 Please input your guess. 99 You guessed: 99 Too big!
使用循环来允许多次猜测
现在游戏已经大体上能玩了,不过用户只能猜一次。增加一个循环来改变它吧!
loop 关键字 创建了一个无限循环 。将其加入后,用户可以反复猜测:
// --snip-- println!("The secret number is: {}", secret_number); loop { println!("Please input your guess."); // --snip-- match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
这样就将提示用户猜测之后的所有内容放入了循环
这里有一个新问题,因为程序忠实地执行了要求:永远地请求另一个猜测,用户好像无法退出啊!
用户总能使用 ctrl-c 终止程序。还有另一个方法跳出无限循环,就是 parse:如果用户输入的答案不是一个数字,程序会崩溃
$ cargo run Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 0.95s Running `target/debug/guessing_game` Guess the number! The secret number is: 39 Please input your guess. 12 You guessed: 12 Too small! Please input your guess. 45 You guessed: 45 Too big! Please input your guess. 39 You guessed: 39 You win! Please input your guess. exit thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:1165:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
输入 exit 确实退出了程序,同时其他任何非数字输入也一样 然而,这并不理想,我们想要当猜测正确的数字时游戏能自动退出
猜测正确后退出
增加一个 break 语句,在用户猜对时退出游戏:
// --snip-- match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } }
通过在 You win! 之后增加一行 break,用户猜对了神秘数字后会退出循环
$ cargo run Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.10s Running `target/debug/guessing_game` Guess the number! The secret number is: 2 Please input your guess. 5 You guessed: 5 Too big! Please input your guess. 1 You guessed: 1 Too small! Please input your guess. 2 You guessed: 2 You win!
处理无效输入
为了进一步改善游戏性,不要在用户输入非数字时崩溃,需要忽略非数字,让用户可以继续猜测。可以通过修改 guess 将 String 转化为 u32 那部分代码来实现:
// --snip-- io::stdin().read_line(&mut guess) .expect("Failed to read line"); let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; println!("You guessed: {}", guess); // --snip--
将 expect 调用换成 match 语句,是从遇到错误就崩溃转换到真正处理错误的惯用方法
parse 返回一个 Result 类型,而 Result 是一个拥有 Ok 或 Err 成员的枚举 这里使用的 match 表达式,和之前处理 cmp 方法返回 Ordering 时用的一样
- 如果 parse 能够成功的将字符串转换为一个数字,它会返回一个包含结果数字的 Ok
- 这个 Ok 值与 match 第一个分支的模式相匹配,该分支对应的动作返回 Ok 值中的数字 num,最后如愿变成新创建的 guess 变量
- 如果 parse 不能将字符串转换为一个数字,它会返回一个包含更多错误信息的 Err
- Err 值不能匹配第一个 match 分支的 Ok(num) 模式,但是会匹配第二个分支的 Err(_) 模式: _ 是一个通配符值,本例中用来匹配所有 Err 值,不管其中有何种信息
- 程序会执行第二个分支的动作, continue 意味着 进入 loop 的下一次循环 ,请求另一个猜测。这样程序就有效的忽略了 parse 可能遇到的所有错误!
- Err 值不能匹配第一个 match 分支的 Ok(num) 模式,但是会匹配第二个分支的 Err(_) 模式: _ 是一个通配符值,本例中用来匹配所有 Err 值,不管其中有何种信息
$ cargo run Compiling guessing_game v0.1.0 (/home/i514692/Documents/programming/html/klose911.github.io/src/rust/src/guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.07s Running `target/debug/guessing_game` Guess the number! The secret number is: 32 Please input your guess. exit Please input your guess. qqq Please input your guess. 12 You guessed: 12 Too small! Please input your guess. 40 You guessed: 40 Too big! Please input your guess. 32 You guessed: 32 You win!
最后,还应该注释掉最开始的生成随机数字的打印语句 :-)