速成课第3节

上一次,我们完成了一个弹跳球实现,但带来了一些缺点: 缺乏亮点的错误处理和难看的缓冲。 它还有另一个限制: 静态帧大小。 今天,我们将解决所有这些问题,从最后一个开始: 让我们获得一些命令行参数来控制帧大小。

这篇文章是基于 FP 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在介绍文章的顶部找到这个系列中所有文章的链接。 也可订阅 RSS 频道。

就像上次一样,我希望你,作为读者,和我一起修改源代码。 一定要在阅读的时候编写代码!

命令行参数

我们将修改我们的应用程序如下:

  • 接受两个命令行参数: width 和 height

  • 都必须是有效的 u32

  • 太多或太少的命令行参数将导致错误消息

听起来很简单。 在实际的应用程序中,我们会使用一个适当的参数处理库,比如 clap。 但是现在,我们正处于初级水平。 就像我们对 sleep 函数所做的一样,让我们从标准库文档中搜索 args 这个词开始。 前两个条目看起来都很相关。

  • std::env::Args 遍历进程参数的迭代器,为每个参数生成一个 String 值。

  • std::env::args 返回该程序启动时使用的参数(通常通过命令行传递)。

按照惯例,现在是提出这个问题的好时机:

  • 模块名称(如 std 和 env)和函数名称(如 args)使用 snake_cased 格式

  • 类型(如 Args)是 PascalCased

    • 例外: 原语如 u32 和 str 是小写的

std 模块有一个 env 模块。 env 模块有一个 Args 类型和一个 Args 函数。 为什么我们两者都需要? 更奇怪的是,让我们来看看 args 函数的类型签名:

pub fn args() -> Args

args 函数返回 Args 类型的值。 如果 Args 是 string 的一个类型同义词,那么这就有意义了。 但事实并非如此。 如果你查看它的文档,在 Args 上没有任何字段或方法,只有 trait 实现!

数据类型模式

也许在 Rust 中有一个合适的术语来形容这个,但是我自己还没有见过。 (如果有的话,请让我知道,这样我才能使用恰当的术语)。 在 Rust 生态系统中有一个普遍的模式,根据我的经验,这个模式从迭代器开始,然后继续到更高级的主题,如 futures 和 async i/o。

  • 我们希望有可组合的接口

  • 我们也想要高性能

  • 因此,我们定义了许多辅助数据类型,允许编译器执行一些优化

  • 我们将 traits 定义为一个接口,使这些类型能够很好地组合在一起

听起来很抽象? 不用担心,我们稍后会把它具体化。以下是所有这些的实际结果:

  • 我们最终针对 traits 编写了相当多的程序,traits 提供了一个通用的抽象和许多辅助函数

  • 我们得到了许多常见函数的匹配数据类型

  • 通常情况下,我们的类型签名最终会非常庞大,代表了我们执行的所有不同的组合(尽管 new-ish-> impl Iterator 风格对此有很大帮助,请参阅公告博客帖子了解更多细节)

好了,解决了这个问题,让我们回到命令行参数!

使用迭代器对 CLI 参数进行遍历

在回到弹跳球之前,让我们在一个空文件中玩一玩。 (要么使用 cargo new 和 cargo run,或直接使用 rustc 调用) 如果我点击 Args docs 页面中 Iterator trait 旁边的展开按钮,我会看到这个函数:

fn next(&mut self) -> Option<String>

让我们来实验一下:

use std::env::args;

fn main() {
    let mut args = args(); // 是的,这个名字很有效
    println!("{:?}", args.next());
    println!("{:?}", args.next());
    println!("{:?}", args.next());
    println!("{:?}", args.next());
}

请注意,我们必须使用 let mut,因为下一个方法将变更值。 现在我要用 cargo run foo bar 运行这个命令:

$ cargo run foo bar
   Compiling args v0.1.0 (/Users/michael/Desktop/tmp/args)
    Finished dev [unoptimized + debuginfo] target(s) in 1.60s
     Running `target/debug/args foo bar`
Some("target/debug/args")
Some("foo")
Some("bar")
None

太棒了! 它给出了可执行文件的名称,后面跟着命令行参数,在没有任何东西剩下时返回 None。 (从技术上讲,命令行参数并不需要将命令名作为第一个参数,它只是大多数工具遵循的一个非常强大的约定)

让我们再试验一下。 你能写一个循环打印出所有的命令行参数然后退出吗? 花点时间,然后我会给出一些答案。

好了,完成了吗? 酷,让我们看一些例子! 首先,我们将循环返回。

use std::env::args;

fn main() {
    let mut args = args();
    loop {
        match args.next() {
            None => return,
            Some(arg) => println!("{}", arg),
        }
    }

我们在这里也不需要使用 return。 我们可以打破循环,而不是从函数返回:

use std::env::args;

fn main() {
    let mut args = args();
    loop {
        match args.next() {
            None => break,
            Some(arg) => println!("{}", arg),
        }
    }
}

或者,如果要节省缩进,可以使用if let。

use std::env::args;

fn main() {
    let mut args = args();
    loop {
        if let Some(arg) = args.next() {
            println!("{}", arg);
        } else {
            break;
            // return也可以,但是break在这里更好,
            // 因为它的范围更窄。
        }
    }
}

你也可以使用 while let,在查看下一个例子之前,试着猜测一下这个例子会是什么样子:

use std::env::args;

fn main() {
    let mut args = args();
    while let Some(arg) = args.next() {
        println!("{}", arg);
    }
}

越来越好! 好吧,最后一个例子:

use std::env::args;

fn main() {
    for arg in args() {
        println!("{}", arg);
    }
}

哇,什么?!? 欢迎来到我最喜欢的 Rust 方面之一。 迭代器是通过 for 循环直接内置在语言中的概念。 for 循环将自动调用 next() 。 这也掩盖了至少在某种程度上存在某种可变状态的事实。 这是一个功能强大的概念,并且允许许多代码最终具有更实用的样式,而我恰好是对此的忠实支持者。

跳跃

以可执行文件的名称作为第一个参数是很好的, 但我们通常不在乎这部分。 我们能不能在输出中略过这一部分? 好吧,这里有一个方法:

use std::env::args;

fn main() {
    let mut args = args();
    let _ = args.next(); // drop it on the floor
    for arg in args {
        println!("{}", arg);
    }
}

这样做是可行的,但是有点笨拙,特别是与我们之前没有可变变量的版本相比。 也许还有别的办法可以跳过这些事情。 让我们再次搜索标准库。 第一个结果是 std::iter::Skipstd::iter::Iterator::skip。 前者是一个数据类型,后者是一个关于 Iterator trait 的方法。 因为我们的 Args 类型实现了 Iterator trait,所以我们可以使用它。 太棒了!

注意:在大多数 Haskell 库中,跳过类似于删除,比如 Data.List 或vector.drop。 在 Rust 中有完全不同的意思(丢弃拥有的数据) ,所以 skip 在 Rust 中是一个更好的名字。

让我们看看上面文档中的一些签名:

pub struct Skip<I> { /* fields omitted */ }
fn skip(self, n: usize) -> Skip<Self>

嗯... 深呼吸。 Skip 是一种数据类型,它参数化了某些数据类型。 这是迭代器中常见的模式: Skip 包装现有数据类型,并为其迭代方式添加一些新功能。 Skip 方法将使用一个现有迭代器,获取要跳过的参数数目,并返回一个新的 Skip <OrigDataType> 值。 我如何知道它消耗原始迭代器? 第一个参数是 “self” ,而不是 “&self” 或 “&mut self”。

这似乎有很多概念,幸运的是,使用起来相当简单:

use std::env::args;

fn main() {
    for arg in args().skip(1) {
        println!("{}", arg);
    }
}

太棒了!

练习1

类型推断使上面的程序在没有任何类型标注的情况下也能正常工作。 但是,最好习惯于生成的类型,因为您会经常在错误消息中看到它们。 通过修复类型签名,获取下面的程序以进行编译。 首先尝试不使用编译器就这样做,因为错误消息几乎可以给出答案。

use std::env::{args, Args};
use std::iter::Skip;

fn main() {
    let args: Args = args().skip(1);
    for arg in args {
        println!("{}", arg);
    }
}

正如上面提到的,这种数据类型分层的方法对性能非常有利。 大量使用迭代器的代码通常会编译成一个高效的循环,可与您手动编写的最佳循环相媲美。 但是,迭代器代码级别更高,更具声明性,易于维护和扩展。

关于迭代器还有很多内容,但是我们暂时就此打住,因为我们仍然需要处理我们的命令行参数,而且我们需要先学习另外一件问题。

解析整数

如果在标准库中搜索 parse,就会找到 str::parse 方法。 文档很好地解释了这个方法,我在这里不再重复。 现在就去读吧。

好吧,你回来了? 涡轮鱼是个有趣的名字,对吧?

尝试编写一个程序,将解析每个命令行参数的结果输出为 u32,然后检查我的版本:

fn main() {
    for arg in std::env::args().skip(1) {
        println!("{:?}", arg.parse::<u32>());
    }
}

让我们尝试运行它:

$ cargo run one 2 three four 5 6 7
Err(ParseIntError { kind: InvalidDigit })
Ok(2)
Err(ParseIntError { kind: InvalidDigit })
Err(ParseIntError { kind: InvalidDigit })
Ok(5)
Ok(6)
Ok(7)

当解析成功时,我们得到 Result 枚举的 Ok 变体。 当解析失败时,我们会得到 Err ,其中有一个 ParseIntError 告诉我们哪里出错了。 (parse 本身上的类型签名使用一些关联的类型来表示这种类型,我们现在不打算讨论这个问题)

这是Rust中的常见模式。 Rust没有运行时异常,因此我们使用实际值在类型级别上跟踪潜在故障。

注:您可能认为恐慌类似于运行时异常,在某种程度上它们确实如此。 但是,您无法从恐慌中正常恢复,这使得它们在实践中与其他语言(如 Python)中使用运行时异常的方式不同。

解析命令行

我们终于准备好开始实际的命令行解析了! 我们的实现过于繁琐,尤其是在数据类型方面。 在空白文件中实现完此代码后,我们将把代码移动到 bouncy 实现本身。 首先,让我们定义一个数据类型以保存成功的解析,其中将包含宽度和高度。

这是一个结构体还是一个枚举? 您能否先尝试自己实现它?

因为我们想保留多个值,所以我们将使用一个结构体。 我想使用命名字段,所以我们有:

struct Frame {
    width: u32,
    height: u32,
}

接下来,让我们定义一个错误类型来表示在此解析过程中可能出错的所有问题。 我们有:

  • 参数太少

  • 太多的参数

  • 无效的整数

这次我们要使用结构体还是枚举?

这一次,我们将使用枚举,因为我们将只检测这些问题中的一个(无论我们首先注意哪个)。 Web 表单和应用解析的官方程序可能会嘲笑这一点,说我们应该检测所有的错误,但是我们会变得懒惰。

enum ParseError {
    TooFewArgs,
    TooManyArgs,
    InvalidInteger(String),
}

请注意,InvalidInteger变量采用有效负载,即解析失败的String。。 这就是为什么 Rust 中的枚举比大多数其他语言中的枚举要强大得多的原因。

我们将编写一个 parse_args 助手函数,您能猜出它的类型签名是什么吗?

综合上面我们建立的所有知识,这里有一个实现:

#[derive(Debug)]
struct Frame {
    width: u32,
    height: u32,
}

#[derive(Debug)]
enum ParseError {
    TooFewArgs,
    TooManyArgs,
    InvalidInteger(String),
}

fn parse_args() -> Result<Frame, ParseError> {
    use self::ParseError::*; // bring variants into our namespace

    let mut args = std::env::args().skip(1);

    match args.next() {
        None => Err(TooFewArgs),
        Some(width_str) => {
            match args.next() {
                None => Err(TooFewArgs),
                Some(height_str) => {
                    match args.next() {
                        Some(_) => Err(TooManyArgs),
                        None => {
                            match width_str.parse() {
                                Err(_) => Err(InvalidInteger(width_str)),
                                Ok(width) => {
                                    match height_str.parse() {
                                        Err(_) => Err(InvalidInteger(height_str)),
                                        Ok(height) => Ok(Frame {
                                            width,
                                            height,
                                        }),
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

fn main() {
    println!("{:?}", parse_args());
}

神圣的嵌套地狱,这是一个很大的缩进! 模式是非常简单的:

  • 模式匹配

  • 如果遇到错误,停止并返回 Err

  • 如果顺利进行,那就继续

Haskellers 在这一点上 notation 和 monads 在尖叫。 忽略他们。 别管他们。 我们在Rust的土地上,我们对这里周围的事物并不友善。 (有人请我对那可怕的双关大喊)

练习2

为什么我们不需要使用 涡轮鱼 来解析上面的内容?

我们想做的是从我们的方法尽早返回。 你知道什么关键词可以帮助吗? 这就对了:return !

fn parse_args() -> Result<Frame, ParseError> {
    use self::ParseError::*;

    let mut args = std::env::args().skip(1);

    let width_str = match args.next() {
        None => return Err(TooFewArgs),
        Some(width_str) => width_str,
    };

    let height_str = match args.next() {
        None => return Err(TooFewArgs),
        Some(height_str) => height_str,
    };

    match args.next() {
        Some(_) => return Err(TooManyArgs),
        None => (),
    }

    let width = match width_str.parse() {
        Err(_) => return Err(InvalidInteger(width_str)),
        Ok(width) => width,
    };

    let height = match height_str.parse() {
        Err(_) => return Err(InvalidInteger(height_str)),
        Ok(height) => height,
    };

    Ok(Frame {
        width,
        height,
    })
}

看起来好多了! 然而,这仍然是一个有点重复,并随处返回那些 Err 主观上并不是很好。 事实上,当我输入这个的时候,我不小心漏掉了一些返回信息,而是盯着一些长长的错误消息。 (你自己试试吧。)

问号(?)

我们将要介绍的结尾 ? 曾经是 try! 宏。 如果你对表面上的重复感到困惑: 这只是一个向新语法的过渡。

上面的模式是如此普遍,以至于 Rust 内置了它的语法。 如果在一个表达式后面加上一个问号(?),那么它基本上完成了整个 match/return-on-err 的事情。 它比我们现在要演示的功能更强大,但是我们稍后会讲到这个额外的功能。

首先,我们将定义一些 helper 函数:

  • 需要另一个参数

  • 要求没有更多的参数

  • 解析 u32

所有这些都需要返回 Result 值,并且我们将对所有这些错误情况使用 ParseError。 前两个函数需要对我们的参数进行可变引用。 (顺便说一句,我现在打算停止使用 skip 方法,因为如果我这样做,它将提供练习1的解决方案)

use std::env::Args;

fn require_arg(args: &mut Args) -> Result<String, ParseError> {
    match args.next() {
        None => Err(ParseError::TooFewArgs),
        Some(s) => Ok(s),
    }
}

fn require_no_args(args: &mut Args) -> Result<(), ParseError> {
    match args.next() {
        Some(_) => Err(ParseError::TooManyArgs),
        // 我觉得这看起来有点怪异。 
        // 但是,我们使用Ok变量包装了value()。
        // 我想你会适应一会儿
        None => Ok(()),
    }
}

fn parse_u32(s: String) -> Result<u32, ParseError> {
    match s.parse() {
        Err(_) => Err(ParseError::InvalidInteger(s)),
        Ok(x) => Ok(x),
    }
}

现在我们已经定义了这些 helpers,我们的 parse_args 函数就更容易查看了:

fn parse_args() -> Result<Frame, ParseError> {
    let mut args = std::env::args();

    // skip the command name
    let _command_name = require_arg(&mut args)?;

    let width_str = require_arg(&mut args)?;
    let height_str = require_arg(&mut args)?;
    require_no_args(&mut args)?;
    let width = parse_u32(width_str)?;
    let height = parse_u32(height_str)?;

    Ok(Frame { width, height })
}

漂亮!

遗忘问号

如果你忘记了 let width_str 行上的问号,你认为会发生什么:

  • width_str 将包含 Result <String,ParseError > 而不是 String

  • 对 parse_u32的调用将不会类型检查

error[E0308]: mismatched types
  --> src/main.rs:50:27
   |
50 |     let width = parse_u32(width_str)?;
   |                           ^^^^^^^^^ expected struct `std::string::String`, found enum `std::result::Result`
   |
   = note: expected type `std::string::String`
              found type `std::result::Result<std::string::String, ParseError>`

很好。 但是,如果我们忘记了 require_no_args 调用上的问号,会发生什么呢? 我们从不使用那里的输出值,所以它会很好地输入 check。 现在我们有一个 c 语言的老问题: 我们不小心忽略了错误代码!

好吧,不要这么快。看看编译器的这个精彩警告:

warning: unused `std::result::Result` which must be used
  --> src/main.rs:49:5
   |
49 |     require_no_args(&mut args);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default
   = note: this `Result` may be an `Err` variant, which should be handled

这是正确的: 如果你忽略了一个潜在的失败,Rust 检测到。 在当前的代码示例中有一个漏洞:

let _command_name = require_arg(&mut args);

这不会触发警告,因为在 let _name = blah; 中,前导下划线表示“我知道我在做什么,我不关心这个值”。 相反,编写代码时最好不要使用 let:

require_arg(&mut args);

现在我们得到一个警告,可以通过添加尾随问号来解决这个问题。

练习3

使用方法调用语法会更方便。让我们定义一个帮助器数据类型来实现这一点。 填写下面代码的实现。

#[derive(Debug)]
struct Frame {
    width: u32,
    height: u32,
}

#[derive(Debug)]
enum ParseError {
    TooFewArgs,
    TooManyArgs,
    InvalidInteger(String),
}

struct ParseArgs(std::env::Args);

impl ParseArgs {
    fn new() -> ParseArgs {
        unimplemented!()
    }


    fn require_arg(&mut self) -> Result<String, ParseError> {
        match self.0.next() {
        }
    }

    fn require_no_args(&mut self) -> Result<(), ParseError> {
        unimplemented!()
    }
}

fn parse_args() -> Result<Frame, ParseError> {
    let mut args = ParseArgs::new();

    // skip the command name
    args.require_arg()?;

    let width_str = args.require_arg()?;
    let height_str = args.require_arg()?;
    args.require_no_args()?;
    let width = parse_u32(width_str)?;
    let height = parse_u32(height_str)?;

    Ok(Frame { width, height })
}

fn main() {
    println!("{:?}", parse_args());
}

更新弹跳球

下一步应该作为一个 Cargo 项目完成,而不是 rustc。让我们开始一个新的空项目:

$ cargo new bouncy-args --bin
$ cd bouncy-args

接下来,让我们获得旧代码并将其放置在 src/main.rs 中。您可以手动复制粘贴,或运行:

$ curl https://gist.githubusercontent.com/snoyberg/5307d493750d7b48c1c5281961bc31d0/raw/8f467e87f69a197095bda096cbbb71d8d813b1d7/main.rs > src/main.rs

运行 cargo Run 并确保它工作正常。您可以使用 Ctrl-C 杀死该程序。

我们已经在上面编写了完全可用的参数解析代码。 与其将其放在同一个源文件中,不如将其放在自己的文件中。 为此,我们将不得不使用Rust中的模块。

为了方便起见,您可以在 Gist 上查看完整的源代码,我们需要将其放入 src/parse _ args.rs:

$ curl https://gist.githubusercontent.com/snoyberg/568899dc3ae6c82e54809efe283e4473/raw/2ee261684f81745b21e571360b1c5f5d77b78fce/parse_args.rs > src/parse_args.rs

如果您现在运行 cargo build,则甚至不会查看 parse_args.rs。 不相信我吗? 在该文件的顶部添加一些无效的内容,然后再次运行 cargo build。 什么都没发生,对吧? 我们需要告诉编译器,我们的项目中还有另一个模块。 我们通过修改 src/main.rs 来实现。 将以下行添加到文件顶部:

mod parse_args;

如果您之前输入了无效行,那么运行 cargo build 现在应该会导致一个错误消息。 好极了!继续前进,删除无效的行,并确保所有内容都能编译和运行。 我们还不会接受命令行参数,但是我们正在接近。

使用它!

由于未使用新模块中的任何内容,因此我们目前收到一些无效代码警告:

warning: struct is never constructed: `Frame`
 --> src/parse_args.rs:2:1
  |
2 | struct Frame {
  | ^^^^^^^^^^^^
  |
  = note: #[warn(dead_code)] on by default

warning: enum is never used: `ParseError`
 --> src/parse_args.rs:8:1
  |
8 | enum ParseError {
  | ^^^^^^^^^^^^^^^

warning: function is never used: `parse_args`
  --> src/parse_args.rs:14:1
   |
14 | fn parse_args() -> Result<Frame, ParseError> {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

让我们来解决这个问题。 首先,在你的 main 函数顶部添加以下内容,以证明我们可以使用我们的新模块:

println!("{:?}", parse_args::parse_args());
return; // 不要开始游戏,我们的输出将消失

另外,在我们想从 main.rs 文件访问的条目前面添加一个 pub,即:

  • struct Frame

  • enum ParseError

  • fn parse_args

让我们运行这个程序:

$ cargo run
   Compiling bouncy-args v0.1.0 (/Users/michael/Desktop/tmp/bouncy-args)
warning: unreachable statement
   --> src/main.rs:115:5
    |
115 |     let mut game = Game::new();
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: #[warn(unreachable_code)] on by default

warning: variable does not need to be mutable
   --> src/main.rs:115:9
    |
115 |     let mut game = Game::new();
    |         ----^^^^
    |         |
    |         help: remove this `mut`
    |
    = note: #[warn(unused_mut)] on by default

    Finished dev [unoptimized + debuginfo] target(s) in 0.67s
     Running `target/debug/bouncy-args`
Err(TooFewArgs)

很高兴我们得到了一个无法访问的语句警告。 不再要求 game 是可变的,这也有点奇怪。 但最重要的是: 我们的参数解析正常工作!

让我们尝试使用这个。我们将修改 Game::new() 方法来接受 Frame 作为输入:

impl Game {
    fn new(frame: Frame) -> Game {
        let ball = Ball {
            x: 2,
            y: 4,
            vert_dir: VertDir::Up,
            horiz_dir: HorizDir::Left,
        };
        Game {frame, ball}
    }

    ...
}

现在我们可以重写我们的 main 函数:

fn main () {
    match parse_args::parse_args() {
        Err(e) => {
            // prints to stderr instead of stdout
            eprintln!("Error parsing args: {:?}", e);
        },
        Ok(frame) => {
            let mut game = Game::new(frame);
            let sleep_duration = std::time::Duration::from_millis(33);
            loop {
                println!("{}", game);
                game.step();
                std::thread::sleep(sleep_duration);
            }
        }
    }
}

不匹配的类型

我们很好,对吧? 不完全的:

error[E0308]: mismatched types
   --> src/main.rs:114:38
    |
114 |             let mut game = Game::new(frame);
    |                                      ^^^^^ expected struct `Frame`, found struct `parse_args::Frame`
    |
    = note: expected type `Frame`
               found type `parse_args::Frame`

现在,我们有两种不同的 Frame 定义:在 parse_args 模块和 main.rs 中。 解决这个问题。 首先,删除 main.rs 中的 Frame 声明。 然后在我们的 mod parse_args; 之后添加以下内容:

use self::parse_args::Frame

self 表示我们正在寻找一个属于当前模块的子模块。

Public 和 private

现在一切都将工作,对不对? 再次错误! cargo build 将吐出一大堆这些错误:

error[E0616]: field `height` of struct `parse_args::Frame` is private
  --> src/main.rs:85:23
   |
85 |         for row in 0..self.frame.height {
   |

默认情况下,Rust 中的标识符是私有的。 为了将它们从一个模块暴露给另一个模块,您需要添加 pub 关键字。 例如:

pub width: u32,

继续并根据需要添加 pub。 最后,如果您运行 cargo run,则应该看到在:“Error parsing args: TooFewArgs“。 如果您运行:cargo run 5 5,则应该看到比以前小得多的边框。 欢呼!

练习4

如果你运行 cargo run 0 0 会发生什么? cargo run 1 1怎么样? 在 parse_args 中加入一些更好的错误处理。

Exit 代码

好吧,最后的刺激。 让我们提供一些无效的参数并检查退出流程的代码:

$ cargo run 5
Error parsing args: TooFewArgs
$ echo $?
0

对于那些不熟悉的人来说: 0的退出代码意味着一切顺利。 这里显然不是这样的! 如果我们搜索标准库,似乎可以使用 std::process::exit 来解决这个问题,尝试用它来解决这个问题。

然而,我们还有一个选择: 我们可以直接从 main 返回一个 Result!

fn main () -> Result<(), self::parse_args::ParseError> {
    match parse_args::parse_args() {
        Err(e) => {
            return Err(e);
        },
        Ok(frame) => {
            let mut game = Game::new(frame);
            let sleep_duration = std::time::Duration::from_millis(33);
            loop {
                println!("{}", game);
                game.step();
                std::thread::sleep(sleep_duration);
            }
        }
    }
}

练习5

您可以在这里做一些清理嵌套的事情吗?

更好的错误处理

我们在上一课中遇到的错误处理问题涉及到调用 top_bottom。 我已经在提供下载的代码中包含了一个解决方案。 猜猜我上次改了什么,然后检查代码以确认你是正确的。

如果您正在密切关注,您可能会惊讶于没有其他 write! 调用的警告! 据我所知,这实际上是Rust编译器中的错误

尽管如此,还是要修正这些调用 write! 的做法,这是一个好习惯。

下一节

到目前为止,仍然没有解决双缓冲问题,我们将在下次讨论。 我们还将从标准库中引入更多的错误处理。 也许可以在迭代器方面做得更多一些。

最后更新于