速成课第4节

我对弹跳球感到无聊,这是我们谈论该计划的第三周。 现在该结束了! 我们如何做双缓冲? 现在该是了解 Rust crates 中外部库的时候了。

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

我们还没有解决双缓冲问题,让我们最终做到这一点。 我们还将从标准库中引入更多的错误处理。 然后我们还将使用迭代器做更多的工作。

查找 crate

为了在终端中进行双缓冲,我们将需要某种 curses 库。 我们将在crates.io上寻找 crate。 在该页面上,搜索“ curses”。 通过查看下载次数和说明,pancurses 看起来不错,特别是因为它支持Unix和Windows。 使用它!

如果你想知道的话,这正是我在写“弹跳球“时候经历的过程。 我没有预先知识告诉我 pancurses 是正确的选择。 此外,这个程序碰巧是我第一次使用 curses 库。

开始一个项目

我们将使用 cargo new 为此创建一个新项目。 我们将从第2周的末尾开始引入弹跳球的代码(在引入命令行解析之前),因为稍后我们将看到不再需要该解析。

$ cargo new bouncy4
$ cd bouncy4
$ curl https://gist.githubusercontent.com/snoyberg/5307d493750d7b48c1c5281961bc31d0/raw/8f467e87f69a197095bda096cbbb71d8d813b1d7/main.rs > src/main.rs
$ cargo run

球应该在你的终端内弹跳,对不对? 太好了!

添加 crate

项目的配置位于 Cargo.toml 文件中,该文件称为清单。 在我的系统中,它看起来像这样:

[package]
name = "bouncy4"
version = "0.1.0"
authors = ["Michael Snoyman <michael@snoyman.com>"]

[dependencies]

它在您的机器上看起来会有点不同(除非您的名字也是 Michael Snoyman)。 注意到最后的 [dependencies] 部分了吗? 这就是我们添加外部 crates 信息的地方。 如果你回到 crates.io 上的 pancurses 页面,你会看到:

Cargo.toml  pancurses = "0.16.0"

当你阅读这篇文章时,情况可能会有所不同。 这告诉我们什么可以添加到依赖库的依赖项部分。 关于如何指定依赖关系有很多细节,我们在这里不会涉及(在速成课中可能永远也不会涉及,我已经花了很多时间讨论依赖关系管理了)。 你可以在 《The Cargo book reference》 阅读更多的内容。

无论如何,继续将 pancurses =“ 0.16.0” 添加到 Cargo.toml 的末尾,然后使用 cargo build 构建。 cargo 运行并进行一些更新,下载,编译,最后得到一个可执行文件,该可执行文件的功能与以前完全相同。 完美,是时候使用它了!

在撰写本文时,pancurses 似乎并未针对 ncurses 软件包的最新版本构建。 如果构建失败,则可能还需要在依赖项中添加 ncurses = “= 5.94.0”,以强制 Cargo 使用较旧的兼容版本。

对于当前版本的 Rust,您还需要在 src/main.rs 的顶部添加以下内容:

extern crate pancurses;

随着 Rust 2018 的出现,这一要求将消失。如果您想使用它(不添加如上内容),则需要切换到 nightly 编译器,然后在 Cargo.toml 中添加 edition = "2018"。 但是,我们将坚持使用稳定的编译器和 Rust 2015。

Library 文档

在 crates.io 页面上,有一个到 pancrases 文档的链接。 在新标签页中打开它。 此外,通过阅读 crates 页,我们得到了一个很好的使用示例。 在 main() 中,第一个调用是 initscr。 跳转到 API 文档并搜索 initscr,最终将看到 initscr 函数

让我们尝试一下,在 main 函数的顶部添加下面这行代码,然后编译:

let window = pancurses::initscr();

太好了! 我们将使用返回的 Window 结构体与 pancras 进行交互。 继续打开它的文档并开始浏览。

获取窗口大小

我们目前仅分配一个任意的窗口大小。 理想情况下,我们希望以实际终端尺寸为基础。 窗口为此提供了一个方法 get_max_yx

首先,为亲爱的读者准备一个步骤:修改 Game 的 new 方法并将 Frame 作为输入参数。 然后,让我们回到 main 。 我们可以通过以下方式捕获最大的 y 和 x 值:

let (max_y, max_x) = window.get_max_yx();

现在,让我们创建一个 Frame 出来。

let frame = Frame {
    width: max_x,
    height: max_y,
};

然后将这个值传递给 Game::new()。 若你已经修改了 new 以获得一个额外的 Frame 参数,那么这个方法将会起作用,如果你错过了这个方法,可以回到前几个段落。

挑战:在你点击编译之前,你能找出我们将要遇到的错误消息吗?

当我运行 cargo build 时,会出现以下错误:

error[E0308]: mismatched types
   --> src/main.rs:109:16
    |
109 |         width: max_x,
    |                ^^^^^ expected u32, found i32

error[E0308]: mismatched types
   --> src/main.rs:110:17
    |
110 |         height: max_y,
    |                 ^^^^^ expected u32, found i32

如果你还记得,width 和 height 都是 u32,但 pancrases 给出的 x 和 y 值都是 i32。 我们如何转换? 解决这个问题的一个简单方法是使用 as u32:

width: max_x as u32,
height: max_y as u32,

这是一个完全未检查的强制转换。为了演示,试着运行这个程序:

fn main() {
    for i in -5i32..6i32 {
        println!("{} -> {}", i, i as u32)
    }
}

(是的,Rust 中有这样的语法,可以为这样的数字跟随类型,这真的很好)。

结果是:

-5 -> 4294967291
-4 -> 4294967292
-3 -> 4294967293
-2 -> 4294967294
-1 -> 4294967295
0 -> 0
1 -> 1
2 -> 2
3 -> 3
4 -> 4
5 -> 5

我们暂时只能接受这种不良行为。别担心,它会变得更糟。

回车和边界

如果你运行这个程序,你会得到一些非常奇怪的输出。 如果你不相信我,现在就自己去查吧。 我等着。

第一个问题是我们的换行符无法按预期运行。 我们以前使用 \n 来创建换行符。 但是,启用 curses 后,这将创建换行符,将光标向下移动一行,而没有回车符,将光标移动到该行的开头。 继续,将 \n 用法替换为 \r\n。

更好,但是仍然存在一个问题:网格不适合终端! 那是因为我们没有考虑边框的大小。 尝试从 x 和 y 值中减去4,看看是否可以解决问题。 (请注意:放置 -4 的方法有些技巧。请考虑您要使用的价值。)

双缓冲

我们仍然没有做过双缓冲,但是现在我们处于更好的位置! 诀窍是更换我们的 println! 在 main 函数的循环中调用 window 方法。 如果您想挑战,请通读文档并尝试弄清楚如何使其发挥作用。 一个提示:您需要还原我们在上面添加的 \r 回车符。

loop {
    window.clear(); // get rid of old content
    window.printw(game.to_string()); // write to the buffer
    window.refresh(); // update the screen
    game.step();
    std::thread::sleep(sleep_duration);
}

呼啦,我们有双重缓冲! 我们终于可以完成这些弹跳球了。

我们可以吗?

练习1

是时候对 API 文档进行更深入的研究,并用更少的训练例子编写一些 Rust 代码了。 我们的实施存在一些问题:

  • 我们不需要为整个网格生成整个字符串。 相反,我们可以使用某些Window方法来移动光标并添加字符。 修改它。

  • 绘制边框要比应有的困难。 Window上有一种方法可以大大帮助您。

  • 我们有很多关于数字的假设,特别是 x 和 y 的最大值为正,并且足够大以保持球的起始位置。 设置一些合理的限制:两个值都必须至少为10。

  • :pancurses具有一些更高级的内置支持,可用于超时的输入处理。 除了使用 std::thread::sleep 之外,我们还可以在主循环中设置输入处理超时。 然后,您还可以响应两种特定的输入:

    • q 按钮退出程序

    • 当窗口大小改变时重置游戏

更多迭代器

好吧,我已经厌倦了弹球。 让我们来谈谈更有趣的东西: 流数据。 就我个人而言,我发现通过自己编写一些迭代器最容易理解迭代器,所以我们将从这里开始。

让我们来做一些编译器引导的编程。 我们之前讨论过有一个 Iterator trait。 因此,我们需要创建一个新的数据类型,并提供 Iterator trait 的实现。 让我们从一些简单的东西开始: 一个根本没有价值的迭代器。

struct Empty;

fn main() {
    for i in Empty {
        panic!("Wait, this shouldn't happen!");
    }
    println!("All done!");
}

panic! 是一种由于不可能情况而导致当前线程退出的方法。 这有点像其他语言中的运行时异常,只是不能从中恢复。 它们只能用于不可能的情况。

编译它,你会得到一个有用的错误消息:

error[E0277]: the trait bound `Empty: std::iter::Iterator` is not satisfied

酷,让我们添加一个空的实现:

impl Iterator for Empty {
}

来自编译器的更多帮助!

error[E0046]: not all trait items implemented, missing: `Item`, `next`
 --> foo.rs:3:1
  |
3 | impl Iterator for Empty {
  | ^^^^^^^^^^^^^^^^^^^^^^^ missing `Item`, `next` in implementation
  |
  = note: `Item` from trait: `type Item;`
  = note: `next` from trait: `fn(&mut Self) -> std::option::Option<<Self as std::iter::Iterator>::Item>`

所以我们需要提供两样东西:Item 和 next。 接下来是一个函数,我们马上就会讲到。 type Item 怎么样? 这就是我们所说的关联类型。 它告诉我们这个迭代器将生成什么类型的值。 因为我们不生产任何东西,我们可以在这里使用任何类型。 我会用 u32:

type Item = u32;

现在我们需要添加一个 next:

fn(&mut Self) -> std::option::Option<<Self as std::iter::Iterator>::Item>

让我们一点一点地简化这个问题。 输入的类型是 &mut Self。 但是,正确的语法是 &mut self。 觉得困惑吗? 请记住 &mut self 是 self: &mut Self 的简写。

fn(&mut self) -> std::option::Option<<Self as std::iter::Iterator>::Item>

接下来,我们可以删除 Option 和 Iterator 的模块限定符,因为它们已经在我们的名称空间中:

fn(&mut self) -> Option<<Self as Iterator>::Item>

Self as Iterator 是非常有趣的。它的意思是 “获取当前类型,并查看它的 Iterator 实现“。 我们关心指定实现的原因是因为 next::Item。 我们真正要说的是“ 我们希望 Item 关联类型与 Iterator 实现相关联”。 有可能其他 trait 也会有一个相同名称的相关类型,所以这是一个明确的方式来指代它。

无论如何,让我们看看这个类型签名是否有效。 包含函数名并给它一个虚函数体:

fn next(&mut self) -> Option<<Self as Iterator>::Item> {
    unimplemented!()
}

unimplemented!() 是一个使用 panic 的宏! 表面上,这是一种方便的方法,可以在活跃的开发过程中去掉实现。 如果你编译这个,它会成功的。 耶! 然后由于 unimplemented!,它在运行时崩溃! 酷。

我们可以通过删除不必要的 as Iterator 来简化签名:

fn next(&mut self) -> Option<Self::Item>

如果你愿意,你也可以直接用 u32 替换 Self::Item。 好的一面是,在这种情况下它更短。 缺点是如果你在将来改变了 Item 的类型,你将不得不在两个地方改变它。 这是一个主观问题,你说了算。

好的,让我们提供一个实现。 我们返回一个 Option,它是一个带有两个变量的枚举: None 或 Some。 前者的意思是 “我们什么都没有” ,后者的意思是“我们有东西” 。假设我们实现了空迭代器,返回 None 似乎是正确的选择:

fn next(&mut self) -> Option<u32> {
    None
}

就这样,我们有了我们的第一个迭代器实现!

练习2

创建一个可以无限生成数字42的迭代器,下面是一个测试它的主函数:

fn main() {
    // only take 10 to avoid looping forever
    for i in TheAnswer.take(10) {
        println!("The answer to life, the universe, and everything is {}", i);
    }
    println!("All done!");
}

可变状态

next 的签名涉及到对 self 的可变引用。 让我们使用它! 我们将创建一个从1到10计数的迭代器。 (如果你觉得自己很勇敢,那么在阅读我的解决方案之前,试着自己实现它)

struct OneToTen(u32);

fn one_to_ten() -> OneToTen {
    OneToTen(1)
}

impl Iterator for OneToTen {
    type Item = u32;

    fn next(&mut self) -> Option<u32> {
        if self.0 > 10 {
            None
        } else {
            let res = Some(self.0);
            self.0 += 1;
            res
        }
    }
}

fn main() {
    for i in one_to_ten() {
        println!("{}", i);
    }
}

练习3:

创建一个生成斐波那契序列的迭代器。 (如果有人听到我对函数式编程领域的这种做法感到惋惜,现在应该会发出由衷的笑声)

Iterator 适

我们还可以编写一个迭代器来修改现有的迭代器。 让我们编写 Doubler,它将使前一个迭代器产生的值翻倍。 为了实现这一点,我们将在新数据类型中捕获底层迭代器,这也需要在所包含的迭代器上参数化 Doubler 数据类型:

struct Doubler<I> {
    iter: I,
}

让我们引入 main 函数来说明如何使用它:

fn main() {
    let orig_iter = 1..11; // array indices start at 1
    let doubled_iter = Doubler {
        iter: orig_iter,
    };
    for i in doubled_iter {
        println!("{}", i);
    }
}

太好了。 如果我们编译它,我们会得到一个关于缺少迭代器实现的错误。 让我们试着写一些东西:

impl Iterator for Doubler {
}

当我们编译这个代码时,我们会得到一个错误消息:

error[E0107]: wrong number of type arguments: expected 1, found 0
 --> foo.rs:5:19
  |
5 | impl Iterator for Doubler {
  |                   ^^^^^^^ expected 1 type argument

好吧,有道理。除非我们为其提供参数,否则Doubler本身不是一种类型。 让我们这样做:

impl Iterator for Doubler<I> {
}

我们收到两个错误消息。 如果愿意,可以随意看第二点,但这并不是很有帮助。 (建议:始终先查看第一条错误消息,然后再尝试解决该问题)。第一条错误消息是:

error[E0412]: cannot find type `I` in this scope
 --> foo.rs:5:27
  |
5 | impl Iterator for Doubler<I> {
  |                           ^ not found in this scope

发生什么事了? 当我们提供一个实现时,我们需要预先声明所有我们想要的类型变量。 那么让我们这样做:

impl<I> Iterator for Doubler<I> {
}

这可能看起来有点多余(起初对我来说是这样) ,但是最终你会遇到更复杂的情况,而且两组尖括号看起来不一样。 (对于 Haskellers 或 PureScript 用户,这有点像要求一个明确的命令)。

好了,现在我们有了一些更接近的东西,编译器很不高兴,因为我们没有给出 Item 和 next 类型。 让我们继续并返回一个 u32:

type Item = u32;
fn next(&mut self) -> Option<u32> {
    unimplemented!()
}

编译并运行,然后由于未实现而崩溃! 太好了, 这里的窍门是我们要向基础 Iterator 询问其下一个值。 我们将通过一些明确的模式匹配来做到这一点(是的,在 Option 上有一个 map 方法,我们可以在这里使用)。

fn next(&mut self) -> Option<u32> {
    match self.iter.next() {
        None => None,
        Some(x) => Some(x * 2),
    }
}

很好,但是当我们编译它的时候,我们被告知:

error[E0599]: no method named `next` found for type `I` in the current scope
 --> foo.rs:8:25
  |
8 |         match self.iter.next() {
  |                         ^^^^
  |
  = help: items from traits can only be used if the trait is implemented and in scope
  = note: the following traits define an item `next`, perhaps you need to implement one of them:
          candidate #1: `std::iter::Iterator`
          candidate #2: `std::str::pattern::Searcher`

编译器知道我们可能指的是 Iterator 中的 next 方法,但是它不使用。 你可能会问为什么? 因为我们从未告诉编译器实现存在! 我们需要指出 I 参数必须具有 Iterator实现。

impl<I: Iterator> Iterator for Doubler<I>

这是一些新的语法,但是非常简单: 我必须有一个迭代器的实现。 不幸的是,我们还没有完全走出困境:

error[E0369]: binary operation `*` cannot be applied to type `<I as std::iter::Iterator>::Item`
  --> foo.rs:10:29
   |
10 |             Some(x) => Some(x * 2),
   |                             ^^^^^
   |
   = note: an implementation of `std::ops::Mul` might be missing for `<I as std::iter::Iterator>::Item`

让我们来看一看。 I 是一个迭代器,我们已经建立了它。 而且我们知道在 x * 2 中使用的 x 值是与 Item 相关的类型。 问题是:我们不知道它是什么,或者它是否可以相乘!

我们已经说过要在这里使用 u32,所以我们可以强制说 Item 是 u32 吗? 是的!

impl<I: Iterator<Item=u32>> Iterator for Doubler<I>

呼,我们的代码工作了!

替代语法: where

随着我们的约束变得越来越复杂,在 impl 开始时将它们全部放入参数中开始感到拥挤。 你也可以选择使用 where:

impl<I> Iterator for Doubler<I>
    where
    I: Iterator<Item=u32>

决定做出这种转变是有主观因素的。 就我个人而言,我更喜欢一致性,而不是简洁,并且几乎总是在哪里使用。 你的目标可能会有所不同, 在阅读他人代码时,您应该注意这两种语法。

不仅仅是 u32

我们被限制在 u32上,这有点奇怪。如果我们把主要功能改成:

let orig_iter = 1..11u64;

我们会得到一个编译错误:

error[E0271]: type mismatch resolving `<std::ops::Range<u64> as std::iter::Iterator>::Item == u32`
  --> foo.rs:23:14
   |
23 |     for i in doubled_iter {
   |              ^^^^^^^^^^^^ expected u64, found u32
   |
   = note: expected type `u64`
              found type `u32`

可以放松一下,但开始变得越来越复杂。让我们开始吧! 我们需要删除实现中对 u32 的所有引用。 下面是第一次尝试:

impl<I> Iterator for Doubler<I>
    where
    I: Iterator
{
    type Item = ???;
    fn next(&mut self) -> Option<Self::Item> {
        match self.iter.next() {
            None => None,
            Some(x) => Some(x * 2),
        }
    }
}

我将 Option<u32> 替换为 Option<self::Item > ,并删除 i: Iterator 上的 <Item=u32> 。 但是对于 Item = ???, 类型我们应该使用什么? 我们希望它与底层迭代器的 Item 类型相同... ... 所以我们就这么说吧!

type Item = I::Item;

成功了! 但是我们仍然没有编译通过,因为 Rust 不知道它可以在 i::Item 上执行乘法。 幸运的是,存在一个可以被成倍增加的trait,称为Mul。 我们可以加入以下内容:

where
I: Iterator,
I::Item: std::ops::Mul,

新的错误消息:

error[E0308]: mismatched types
  --> foo.rs:14:29
   |
14 |             Some(x) => Some(x * From::from(2u8)),
   |                             ^^^^^^^^^^^^^^^^^^^ expected std::iter::Iterator::Item, found std::ops::Mul::Output
   |
   = note: expected type `<I as std::iter::Iterator>::Item`
              found type `<<I as std::iter::Iterator>::Item as std::ops::Mul>::Output`

事实证明,Mul 的输出有一个关联的类型。 这对于在类型级别表达更复杂的关系非常有用。 例如,我们可以定义力、质量和加速度的类型,然后定义一个 Mul 实现,即质量乘以加速度产生一个力。

这是一个很棒的功能,但是在这里已经成为我们的障碍。 我们想说“输出应该与 Item 本身相同” 。有道理:

impl<I> Iterator for Doubler<I>
    where
    I: Iterator,
    I::Item: std::ops::Mul<Output=I::Item>,

现在:

error[E0308]: mismatched types
  --> foo.rs:14:33
   |
14 |             Some(x) => Some(x * 2),
   |                                 ^ expected associated type, found integral variable
   |
   = note: expected type `<I as std::iter::Iterator>::Item`
              found type `{integer}`

呃。 我们有2,可以是某种整型。 但是我们不知道 Item 是不是整数类型! 我不知道有什么方法可以提供一个约束来允许这段代码工作(如果有人知道的更多,请让我知道,我会更新这段文本)。 一个有效的方法是使用 From trait 从 u8 向上转换,它执行安全的数字转换(不能溢出或下溢) :

impl<I> Iterator for Doubler<I>
    where
    I: Iterator,
    I::Item: std::ops::Mul<Output=I::Item> + From<u8>,
{
    type Item = I::Item;
    fn next(&mut self) -> Option<Self::Item> {
        match self.iter.next() {
            None => None,
            Some(x) => Some(x * From::from(2u8)),
        }
    }
}

呼,终于结束了!

练习4

一个简单的方法是使用 x + x 而不是 x * 2。 重写迭代器来做到这一点。 一个提示: 除非通过恰当命名的 trait 告诉编译器,否则编译器不会知道它被允许复制该类型。

简要回顾

太多了,但是希望它能使人们了解迭代器的工作原理。我们可以编写高度通用的适配器来处理多种输入。 您可以像我们一样对范围迭代器应用 Doubler。 您可以将它应用于我们前面定义的 Empty。 或者其他很多事情。

您可能注意到这些迭代器的类型似乎随着向它们添加更多的内容而增加。 这是千真万确的,而且也是有意为之。 通过静态地表示完整类型,编译器能够看到迭代器操作流水线中正在执行的所有操作,并能很好地优化内容。

更习惯的方式

我们写的 Doubler 根本不是惯用方式。 让我们以实际的方式来做:

fn main() {
    for i in (1..11).map(|x| x * 2) {
        println!("{}", i);
    }
}

Iterator trait 包含许多 helper 方法,所以你可以像这样链接大量的操作:

fn main() {
    for i in (1..11).skip(3).map(|x| x + 1).filter(|x| x % 2 == 0) {
        println!("{}", i);
    }
}

当然,你可以在 c/c++ 中编写类似于 for 循环的代码,但是:

  • 这样就更难理解其中的逻辑

  • 未来的扩展将更加困难

  • 它不会更快,Rust 编译器将优化这样的情况,将下降到与手动编写的循环相媲美

收集 Result

你可以从向量中的迭代器中收集 Result:

fn main() {
    let my_vec: Vec<u32> = (1..11).collect();
    println!("{:?}", my_vec);
}

这里需要类型注释,因为 collect 可以处理许多不同的数据类型。

练习5

使用 fold 方法得到从1到10的数字之和。 额外写一个辅助求和函数。

下一节

我们已经在闭包上玩了很长一段时间了,包括最后一个练习。 我们现在知道足以学习他们了。 下次我们会这样做。

您现在已经到了可以编写一些真正的 Rust 应用程序并开始实践的时候了。 我建议你找一些你想尝试的游戏任务。 如果你正在寻找一些挑战,Exercism 可能是一个不错的选择。

最后更新于