第一个完整的工程——解析《Rust程序设计语言第二版中文版》中猜猜看游戏代码

孟雪风
2023-12-01


《Rust程序设计语言第二版中文版》中,把猜猜看游戏放在了第二章讲解,我个人觉得还是学完了基本的语法后再回来学习。现在,终于可以用这个实例来感受Rust的风采了。
这个实例可谓麻雀虽小,五脏俱全,包含了使用cargo进行工程管理、包管理以及使用Rust引用库和对标准输入输出的处理。

工程管理

创建工程

使用cargo new命令创建新的工程,先来使用cargo help new命令看一下new的参数:

cargo.exe-new
Create a new cargo package at <path>

USAGE:
    cargo.exe new [OPTIONS] <path>

OPTIONS:
    -q, --quiet                  No output printed to stdout
        --registry <REGISTRY>    Registry to use
        --vcs <VCS>              Initialize a new repository for the given version control system (git, hg, pijul, or
                                 fossil) or do not initialize any version control at all (none), overriding a global
                                 configuration. [possible values: git, hg, pijul, fossil, none]
        --bin                    Use a binary (application) template [default]
        --lib                    Use a library template
        --edition <YEAR>         Edition to set for the crate generated [possible values: 2015, 2018]
        --name <NAME>            Set the resulting package name, defaults to the directory name
    -v, --verbose                Use verbose output (-vv very verbose/build.rs output)
        --color <WHEN>           Coloring: auto, always, never
        --frozen                 Require Cargo.lock and cache are up to date
        --locked                 Require Cargo.lock is up to date
        --offline                Run without accessing the network
    -Z <FLAG>...                 Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help                   Prints help information

ARGS:
    <path>

可以看到cargo支持版本控制,除了git,其它的都没听说过,而且不支持svn,难道svn已经被时代无情的抛弃了?
cargo还为工程提供了两种的模板,可以通过参数来选择模板。

  • –bin:可执行程序模板
  • –lib:动态库程序模板
    下面我们来创建猜猜看工程:
cargo new --bin guessing
     Created binary (application) `guessing` package

执行完成后,在当前目录下生成了guessing目录,目录结构如下:

hellorust
├─ .git 
├─ .gitignore
├─ Cargo.toml
└─ src
   └─ main.rs

cargo默认使用git作为版本控制器,在生成代码文件的同时,还生成了一个Cargo.toml。
Cargo.toml文件是该工程的说明文档,内容如下:

[package]
name = "guessing"
version = "0.1.0"
authors = ["zhangmh <zhangmh@sdysit.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

不知道我的信息是从哪里读取的。
根据选择的模板,main.rs中的原始代码也不相同,可执行程序的main.rs内容如下:

fn main() {
    println!("Hello, world!");
}

编译工程

使用cargo build命令可以对工程进行编译。同样的,先用help看一下:

cargo.exe-build
Compile a local package and all of its dependencies

USAGE:
    cargo.exe build [OPTIONS]

OPTIONS:
    -q, --quiet                      No output printed to stdout
    -p, --package <SPEC>...          Package to build (see `cargo help pkgid`)
        --all                        Alias for --workspace (deprecated)
        --workspace                  Build all packages in the workspace
        --exclude <SPEC>...          Exclude packages from the build
    -j, --jobs <N>                   Number of parallel jobs, defaults to # of CPUs
        --lib                        Build only this package's library
        --bin <NAME>...              Build only the specified binary
        --bins                       Build all binaries
        --example <NAME>...          Build only the specified example
        --examples                   Build all examples
        --test <NAME>...             Build only the specified test target
        --tests                      Build all tests
        --bench <NAME>...            Build only the specified bench target
        --benches                    Build all benches
        --all-targets                Build all targets
        --release                    Build artifacts in release mode, with optimizations
        --profile <PROFILE-NAME>     Build artifacts with the specified profile
        --features <FEATURES>...     Space-separated list of features to activate
        --all-features               Activate all available features
        --no-default-features        Do not activate the `default` feature
        --target <TRIPLE>            Build for the target triple
        --target-dir <DIRECTORY>     Directory for all generated artifacts
        --out-dir <PATH>             Copy final artifacts to this directory (unstable)
        --manifest-path <PATH>       Path to Cargo.toml
        --message-format <FMT>...    Error format
        --build-plan                 Output the build plan in JSON (unstable)
    -v, --verbose                    Use verbose output (-vv very verbose/build.rs output)
        --color <WHEN>               Coloring: auto, always, never
        --frozen                     Require Cargo.lock and cache are up to date
        --locked                     Require Cargo.lock is up to date
        --offline                    Run without accessing the network
    -Z <FLAG>...                     Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help                       Prints help information

All packages in the workspace are built if the `--workspace` flag is supplied. The
`--workspace` flag is automatically assumed for a virtual manifest.
Note that `--exclude` has to be specified in conjunction with the `--workspace` flag.

Compilation can be configured via the use of profiles which are configured in
the manifest. The default profile for this command is `dev`, but passing
the --release flag will use the `release` profile instead.

编译的功能非常完善,支持多CPU编译,多目标编译等等。当然,我们的这个实例非常简单,不需要这些高级的功能:

cd guessing
cargo build
   Compiling guessing v0.1.0 (D:\test\rust\guessing)
    Finished dev [unoptimized + debuginfo] target(s) in 1.18s

默认的,cargo编译使用debug的方式,不进行优化,且包含调试信息。可以指定以release的方式编译,进行优化并去除调试信息。

cargo build --release
   Compiling guessing v0.1.0 (D:\test\rust\guessing)
    Finished release [optimized] target(s) in 2.26s

编译完成后,生成的exe文件在target目录中,根据编译方式的不同,保存在debug或release目录中。
Rust编译会将运行时需要的所有信息全部打包到exe中,可以完全脱离Rust环境运行,但exe文件也比较大。

运行工程

当然可以找到编译出来的exe运行,但有更简单的方法,直接使用cargo run命令运行。先看帮助:

cargo.exe-run
Run a binary or example of the local package

USAGE:
    cargo.exe run [OPTIONS] [--] [args]...

OPTIONS:
    -q, --quiet                      No output printed to stdout
        --bin <NAME>...              Name of the bin target to run
        --example <NAME>...          Name of the example target to run
    -p, --package <SPEC>             Package with the target to run
    -j, --jobs <N>                   Number of parallel jobs, defaults to # of CPUs
        --release                    Build artifacts in release mode, with optimizations
        --profile <PROFILE-NAME>     Build artifacts with the specified profile
        --features <FEATURES>...     Space-separated list of features to activate
        --all-features               Activate all available features
        --no-default-features        Do not activate the `default` feature
        --target <TRIPLE>            Build for the target triple
        --target-dir <DIRECTORY>     Directory for all generated artifacts
        --manifest-path <PATH>       Path to Cargo.toml
        --message-format <FMT>...    Error format
    -v, --verbose                    Use verbose output (-vv very verbose/build.rs output)
        --color <WHEN>               Coloring: auto, always, never
        --frozen                     Require Cargo.lock and cache are up to date
        --locked                     Require Cargo.lock is up to date
        --offline                    Run without accessing the network
    -Z <FLAG>...                     Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help                       Prints help information

ARGS:
    <args>...

If neither `--bin` nor `--example` are given, then if the package only has one
bin target it will be run. Otherwise `--bin` specifies the bin target to run,
and `--example` specifies the example target to run. At most one of `--bin` or
`--example` can be provided.

All the arguments following the two dashes (`--`) are passed to the binary to
run. If you're passing arguments to both Cargo and the binary, the ones after
`--` go to the binary, the ones before go to Cargo.

在执行cargo run时,cargo会先检查目录是否存在,若不存在,则先进行编译,然后再运行。所以run的选项和build比较像。
为了演示先编译后运行的效果,先用cargo clean命令清除编译后的文件,再执行cargo run

cargo clean
cargo run
   Compiling guessing v0.1.0 (D:\test\rust\guessing)
    Finished dev [unoptimized + debuginfo] target(s) in 0.82s
     Running `target\debug\guessing.exe`
Hello, world!

输出了Hello, world!,执行成功。

包管理

cargo不旦可以进行工程管理,还可以像python的pip那样进行包管理。

在记录如何管理包之前,先总结一下Rust中关于包的概念。
cargo new创建的,就是一个包(package),包中含有crate,crate根据new命令的参数分为两种:

  • library crate:库
  • binary crate:可执行程序

包中包含crate的规则如下:

  1. 包中至少包含一个 crate
  2. 一个包中至多只能包含一个库(library crate);
  3. 一个包中可以包含任意多个可执行程序(binary crate);
    我们引用的包,大多数都是包含library crate的,我们可以调用library crate里面的特性(trait)。特性(trait)的概念类似于C/C++的namespace。

使用cargo install命令可安装指定的crate。

cargo.exe-install
Install a Rust binary. Default location is $HOME/.cargo/bin

USAGE:
    cargo.exe install [OPTIONS] [--] [crate]...

OPTIONS:
    -q, --quiet                     No output printed to stdout
        --version <VERSION>         Specify a version to install
        --git <URL>                 Git URL to install the specified crate from
        --branch <BRANCH>           Branch to use when installing from git
        --tag <TAG>                 Tag to use when installing from git
        --rev <SHA>                 Specific commit to use when installing from git
        --path <PATH>               Filesystem path to local crate to install
        --list                      list all installed packages and their versions
    -j, --jobs <N>                  Number of parallel jobs, defaults to # of CPUs
    -f, --force                     Force overwriting existing crates or binaries
        --no-track                  Do not save tracking information (unstable)
        --features <FEATURES>...    Space-separated list of features to activate
        --all-features              Activate all available features
        --no-default-features       Do not activate the `default` feature
        --profile <PROFILE-NAME>    Install artifacts with the specified profile
        --debug                     Build in debug mode instead of release mode
        --bin <NAME>...             Install only the specified binary
        --bins                      Install all binaries
        --example <NAME>...         Install only the specified example
        --examples                  Install all examples
        --target <TRIPLE>           Build for the target triple
        --root <DIR>                Directory to install packages into
        --registry <REGISTRY>       Registry to use
    -v, --verbose                   Use verbose output (-vv very verbose/build.rs output)
        --color <WHEN>              Coloring: auto, always, never
        --frozen                    Require Cargo.lock and cache are up to date
        --locked                    Require Cargo.lock is up to date
        --offline                   Run without accessing the network
    -Z <FLAG>...                    Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help                      Prints help information

ARGS:
    <crate>...

This command manages Cargo's local set of installed binary crates. Only
packages which have executable [[bin]] or [[example]] targets can be
installed, and all executables are installed into the installation root's
`bin` folder. The installation root is determined, in order of precedence, by
`--root`, `$CARGO_INSTALL_ROOT`, the `install.root` configuration key, and
finally the home directory (which is either `$CARGO_HOME` if set or
`$HOME/.cargo` by default).

There are multiple sources from which a crate can be installed. The default
location is crates.io but the `--git`, `--path`, and `--registry` flags can
change this source. If the source contains more than one package (such as
crates.io or a git repository with multiple crates) the `<crate>` argument is
required to indicate which crate should be installed.

Crates from crates.io can optionally specify the version they wish to install
via the `--version` flags, and similarly packages from git repositories can
optionally specify the branch, tag, or revision that should be installed. If a
crate has multiple binaries, the `--bin` argument can selectively install only
one of them, and if you'd rather install examples the `--example` argument can
be used as well.

By default cargo will refuse to overwrite existing binaries. The `--force` flag
enables overwriting existing binaries. Thus you can reinstall a crate with
`cargo install --force <crate>`.

Omitting the <crate> specification entirely will install the crate in the
current directory. This behaviour is deprecated, and it no longer works in the
Rust 2018 edition. Use the more explicit `install --path .` instead.

If the source is crates.io or `--git` then by default the crate will be built
in a temporary target directory. To avoid this, the target directory can be
specified by setting the `CARGO_TARGET_DIR` environment variable to a relative
path. In particular, this can be useful for caching build artifacts on
continuous integration systems.

只不过,cargo install命令只能安装带有可执行程序的crate,这个实例用到了rand库,用来生成随机数,这个库里面没有可执行程序。执行如下命令

cargo install rand
    Updating `git://mirrors.ustc.edu.cn/crates.io-index` index
  Downloaded rand v0.7.2 (registry `git://mirrors.ustc.edu.cn/crates.io-index`)
  Downloaded 1 crate (111.4 KB) in 1.71s
error: specified package `rand v0.7.2` has no binaries

最后cargo报错,说rand v0.7.2没有编译好的二进制文件。我们可以使用另外的方式进行包管理。
修改Cargo.toml文件,在[dependencies]中添加如下内容:

rand = "0.7.2"

使用cargo进行编译:

cargo build
   Compiling getrandom v0.1.14
   Compiling cfg-if v0.1.10
   Compiling ppv-lite86 v0.2.6
   Compiling rand_core v0.5.1
   Compiling c2-chacha v0.2.3
   Compiling rand_chacha v0.2.1
   Compiling rand v0.7.2
   Compiling guessing v0.1.0 (D:\test\rust\guessing)
    Finished dev [unoptimized + debuginfo] target(s) in 7.36s

cargo帮我们下载了rand v0.7.2以及它所依赖的其他包。当然,我们在cargo install时,虽然执行失败了,但我知道了它最新的版本号是0.7.2,如果不知道该版本,可以使用

cargo search rand
rand = "0.7.2"              # Random number generators and other randomness functionality.
random_derive = "0.0.0"     # Procedurally defined macro for automatically deriving rand::Rand for structs and enums
ndarray-rand = "0.11.0"     # Constructors for randomized arrays. `rand` integration for `ndarray`.
fake-rand-test = "0.0.0"    # Random number generators and other randomness functionality.
rand_derive = "0.5.0"       # `#[derive(Rand)]` macro (deprecated).
rand_core = "0.5.1"         # Core random number generator traits and tools for implementation.
pcg_rand = "0.11.1"         # An implementation of the PCG family of random number generators in pure Rust
derive_rand = "0.0.0"       # `#[derive]`-like functionality for the `rand::Rand` trait.
rand_macros = "0.1.10"      # `#[derive]`-like functionality for the `rand::Rand` trait.
rand_seeder = "0.2.0"       # A universal random number seeder based on SipHash.
... and 278 crates more (use --limit N to see more)

命令查询,也可以不填写版本号:

[dependencies]
rand = ""

这样cargo会自动安装最新版本。不过,不推荐使用这种方式,因为某些库的升级可能会对已经完成的代码造成影响。
配置完了依赖,就可以在代码中使用了,先上代码:

use rand::Rng;

fn main() 
{
    let number = rand::thread_rng().gen_range(1, 999);

    println!("number is {}", number);
}

第一行,use类似于C/C++的include,rand是crate名,Rng是trait名。这一行的作用是引用了rand的Rng。
第五行调用rand产生一个1~999的随机数,将该数绑定到number变量。

标准IO

要在Rust中使用标准输入输出,要引入标准库中的一个crate:io。因为是标准库,所以不需要指定依赖,直接使用use引入就可以了。

标准输入

use std::io;

fn main()
{
    let mut guess = String::new();
    println!("input your guess:");
    io::stdin().read_line(&mut guess);

    println!("you guess:{}", guess);
}

要接收标准输入,首先要创建一个字符串用于接收输入的内容,该字符串应该是可变的。
有了接收的字符串,就可以使用io::stdin().read_line()来读取标准输入,以可变的引用的方式将读取到的内容保存到接收字符串中。
上面的代码已经可以运行,但编译时会有一个警告:

warning: unused `std::result::Result` that must be used
  --> src\main.rs:17:5
   |
17 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

这个警告涉及到了Rust的错误处理机制。这里先粗略记录一下,真正学到错误处理时再详细记录。
Rust代码运行时,会发生两种错误:

  • 可恢复的错误:Result
  • 不可恢复的错误:panic

这个警报的意思是说这段代码可能会发生Result类型的错误,需要进行错误处理。在这个例子中,错误处理非常简单,只需要在io::stdin().read_line(&mut guess)后增加一句.expect(“Failed to read line”),意思是当发生读取错误时,打印 “Failed to read line” 。修改完后的代码为:

use std::io;

fn main()
{
    let mut guess = String::new();
    println!("input your guess:");
    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("you guess:{}", guess);
}

这样就不会有那个警告了。

标准输出

标准输出已经不陌生了,在前面的例子中,使用了很多println!宏进行输出。
println!宏的代码如下:

#[macro_export]
#[stable(feature = "rust1", since = "1.0.0")]
#[allow_internal_unstable(print_internals, format_args_nl)]
macro_rules! println {
    () => ($crate::print!("\n"));
    ($($arg:tt)*) => ({
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    })
}

现在的我还看不太懂这段代码,总之,println!宏使用了io::_print实现的标准输出。
我们还可以使用如下的方式进行输出:

use std::io::Write;

fn main()
{
    std::io::stdout().write("hello".as_bytes())
        .expect("Failed to read line");
}

当然,这远没有println!宏好用。

完整的猜猜看游戏

有了前面的知识,我们就可以写出完整的猜猜看游戏了。我没有看教程的代码,自己按自己的想法写了一个,代码如下:

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

fn main()
{
    let number = rand::thread_rng().gen_range(1, 999);

    loop
    {
        println!("猜猜看这个三位数是多少呢:");
        let mut guess = String::new();
        io::stdin().read_line(&mut guess)
            .expect("读取输入失败。");
        println!("您猜的是{}", guess);

        guess = guess.trim().to_string();

        // 输入合法性校验
        if !is_interger(&guess)
        {
            println!("请输入数字");
            continue;
        }
        
        let gn = guess.parse::<i32>().unwrap();     // 字符串转为i32,read_line读取的带有回车,会造成类型转换失败,需要先进行trim
        if gn == number
        {
            println!("恭喜你猜对了!");
            break;
        }
        else if gn < number
        {
            println!("再大点儿。");
        }
        else
        {
            println!("再小点儿。");
        }
    }
}

fn is_interger(s:&String) -> bool
{
    for c in s.chars()
    {
        println!("{}, {}", c, c <= '0' || c >= '9');
        if c <= '0' || c >= '9'
        {
            return false;
        }
    }

    return true;
}

这一部分的内容对于我确实早了一点,写的过程中发现了与move语义及借用和引用的问题,查阅了后面所有权的内容以后才得以解决。好在第一个实例还是写出来了,但与教程中的代码相比,我们代码实在丑陋不堪。python有pythonic,Rust是不是也有rustic一说呢?
实例中的例子对我有很大的启发,整段代码一个if都没有,使用了两次模式匹配解决问题:

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() 
{
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1, 101);
    loop 
    {
        println!("Please input your guess.");
        let mut guess = String::new();
        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);
        match guess.cmp(&secret_number) 
        {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => 
            {
                println!("You win!");
                break;
            }
        }
    }
}

第一处使用模式匹配是在处理字符串到数字的转换时。parse返回一个Result类型,而Result是一个拥有Ok或Err成员的枚举。使用match对Result进行模式匹配是从遇到错误就崩溃转换到真正处理错误的惯用方法。
第二处使用模式匹配是在比较大小时。cmp返回一个Ordering类型,而Ordering类型是一个拥有Less、Greater、Equal三个成员的枚举,这也与模式匹配的思想契合。

要适应Rust的思想,真的需要好好修炼才行。

 类似资料: