UP | HOME

入门

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!");

这行代码完成这个简单程序的所有工作:在屏幕上打印文本。这里有四个重要的细节需要注意:

  1. Rust 的缩进风格使用 4 个空格 ,而不是 1 个制表符
  2. println! 调用了一个 Rust 宏 。如果是调用函数,则应输入 println ( 没有! )
  3. "Hello, world!" 是一个字符串。把这个字符串作为一个参数传递给 println!,字符串将被打印到屏幕上
  4. 该行以 分号结尾 ; ,这代表一个表达式的结束和下一个表达式的开始。大部分 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 那一行的 :: 语法表明 newString 类型的一个 关联函数 ,关联函数是针对类型实现的

一些语言中把它称为 静态方法

在这个例子中 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 的成员是 OkErr

  • 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 之间的数
  • 第二行就是打印出这个随机数字
$ 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 也是一个 枚举 ,不过它的成员是 LessGreater+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 有一些内建的数字类型;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 可能遇到的所有错误!
$ 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!
      最后,还应该注释掉最开始的生成随机数字的打印语句 :-) 

总结

     这里介绍了 Rust 新概念:let、match、方法、关联函数、使用外部 crate 等等

     以后还会介绍rust最与众不同的一点是“它关于指针的所有权”

Next: 概念

Home: 目录