速成课第2节

可以说 Rust 与其他流行编程语言最大的区别在于它的所有权模型。 在本课中,我们将接触 Rust 的所有权基础知识。 你可以在 Rust Book 中学习到更多关于所有权的章节

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

格式

我将尝试一些课程格式。 我想涵盖以下两个方面:

  • 关于所有权更多理论讨论

  • 尝试实际编写程序

随着时间的推移,我打算花更多的时间在后者上,而不是前者,尽管我们现在仍然需要花大量的时间在前者上。 我将在这篇文章的开头讨论所有权,然后我们将实现第一个版本的 “弹跳球“。

如果这种方法对大家有效的话,可能会让大家觉得有点拘谨,反馈也是值得赞赏的。

与 Haskell 的比较

我将首先比较 Rust 和 Haskell,因为两种语言都有一个强大的不变性概念。 然而,Haskell 是一种垃圾回收语言。 让我们来看看这两种语言的比较。在 Haskell 中:

  • 默认情况下一切都是不可变的

  • 您可以使用显式的可变包装器(如IORefMVar)来标记可变性

  • 您可以随意对数据的引用进行共享

  • 垃圾回收不确定地释放内存

  • 当您需要确定性的资源处理(例如文件句柄)时,需要使用方括号模式或类似的模式

在 Rust 中,数据所有权更为重要:它是允许语言绕过垃圾收集的主要内容。 它还允许数据经常驻留在堆栈上而不是堆上,从而提高性能。 此外,它可以确定地运行,这使它成为处理其他资源(如文件句柄)的好方法。

所有权从以下规则开始:

  • Rust 中的每个值都会赋值给一个变量,称为它的所有者

  • 一次只能有一个所有者

  • 当所有者超出作用域时,该值将被删除。

简易示例

考虑下面的代码:

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
}

语法注释:#[...] 是编译器编译指示。 #[derive(...)] 是一个示例,类似于在 Haskell 中对 typeclasses 使用派生。 对于某些 Traits,如果您要求,编译器可以自动提供实现。

语法注释:格式字符串中的语法注释 {:?} 表示“使用 Debug Traits显示此内容”

Foobar 是所谓的新类型包装器:它是一种新的数据类型,它以带符号的32位整数(i32)包裹并且具有相同生命周期的表示形式。

在 main 函数中,x 包含 Foobar。 当调用 uses_foobar 时,会将该 Foobar 的所有权将传递给 uses_foobar。 在 main 中再次使用该 x 将会是一个错误:

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
    uses_foobar(x);
}

结果如下:

error[E0382]: use of moved value: `x`
  --> foo.rs:11:17
   |
10 |     uses_foobar(x);
   |                 - value moved here
11 |     uses_foobar(x);
   |                 ^ value used here after move
   |
   = note: move occurs because `x` has type `Foobar`, which does not implement the `Copy` trait

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.

销毁

当一个值超出作用域时,使用它的 Drop trait (类似 typeclass) ,然后释放内存。 我们可以通过为 Foobar 编写一个 Drop 实现来说明这一点:

在看到输出之前,猜猜下面这个程序的输出是什么。

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    println!("Before uses_foobar");
    uses_foobar(x);
    println!("After uses_foobar");
}

输出:

Before uses_foobar
I consumed a Foobar: Foobar(1)
Dropping a Foobar: Foobar(1)
After uses_foobar

注意,该值在 use_foobar 之后被删除。 这是因为该值被移动到 use_foobar 函数中,并且当该函数退出时,将调用 drop。

练习1

在标准库中有一个函数 std: : mem: : drop,它会立即删除一个值。

作用域

Rust 中的作用域目前是非作用域生命周期,尽管有一个非作用域生命周期(None Lexical Lifetimes,NLL)提案正在被合并。 (在 Stack Overflow 上有一个关于 NLL 的很好的解释)。 我们可以证明当前作用域的性质:

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn main() {
    println!("Before x");
    let _x = Foobar(1);
    println!("After x");
    {
        println!("Before y");
        let _y = Foobar(2);
        println!("After y");
    }
    println!("End of main");
}

语法说明: 对未使用的变量使用 _。

这个程序的输出是:

Before x
After x
Before y
After y
Dropping a Foobar: Foobar(2)
End of main
Dropping a Foobar: Foobar(1)

删除看似多余的大括号并运行程序。 在查看实际输出之前,猜测输出将是什么。

Borrows / references

有时候你希望能够在不改变所有权的情况下共享对某个值的引用。很简单:

#[derive(Debug)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping a Foobar: {:?}", self);
    }
}

fn uses_foobar(foobar: &Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(&x);
    println!("After uses_foobar");
}

注意事项:

  • uses_foobar 获取类型 &Foobar 的值,它是“对 Foobar 的不可变引用“

  • use_foobar 内部,我们不需要显式地取消引用 foobar 值,这是由 Rust 自动完成的

  • 在 main 中,我们现在可以使用 x 来调用 uses_foobar 两次

  • 为了从值创建一个引用,我们在变量前面使用 &

挑战: 您认为何时打印 “ Dropping a Foobar:” 消息?

还记得上一节课中提到的一个参数 self 的特殊语法吗。 drop 上的签名看起来与 use_fobar 完全不同。 当您看到 &mut self 时,您可以将其视为self: &mut Self。 现在它看起来更类似于 use_foobar。

练习2

我们也希望能够为 uses_foobar 使用对象语法。 在 Foobar 类型上创建方法 use_it,以打印 "I consumed" 的信息。

提示: 你需要在 impl Foobar { ... }里面操作。

多存活引用

我们可以更改 main 函数,以允许对 x 同时存在两个引用。 这个版本还在局部变量上添加了显式类型,而不是依赖于类型推断:

fn main() {
    let x: Foobar = Foobar(1);
    let y: &Foobar = &x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(y);
    println!("After uses_foobar");
}

这在 Rust 中是允许的,因为:

  • 对一个变量的多个只读引用不能导致任何数据竞争

  • 值的生存期比对它的引用更长。 换句话说,在这种情况下,x 的寿命至少和 y 一样长。

让我们看看打破这一局面的两种方法。

引用未存活的值

还记得之前的 std::mem::drop 吗:

fn main() {
    let x: Foobar = Foobar(1);
    let y: &Foobar = &x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    std::mem::drop(x);
    uses_foobar(y);
    println!("After uses_foobar");
}

这将导致错误消息:

error[E0505]: cannot move out of `x` because it is borrowed
  --> foo.rs:19:20
   |
16 |     let y: &Foobar = &x;
   |                       - borrow of `x` occurs here
...
19 |     std::mem::drop(x);
   |                    ^ move out of `x` occurs here

error: aborting due to previous error

For more information about this error, try `rustc --explain E0505`.

引用其他的可变引用

您还可以对值进行可变引用。 为了避免数据竞赛,Rust 不允许以任何其他方式同时引用和访问值。

fn main() {
    let mut x: Foobar = Foobar(1);
    let y: &mut Foobar = &mut x;
    println!("Before uses_foobar");
    uses_foobar(&x); // will fail!
    uses_foobar(y);
    println!("After uses_foobar");
}

注意 y 的类型现在是 &mut Foobar。 像Haskell一样,Rust在类型级别跟踪可变性。 好极了!

挑战

尝试猜测下面代码中的哪些行会触发编译错误:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);

    foo(x);
    foo(x);

    let mut y = Foobar(2);

    bar(&y);
    bar(&y);

    let z = &mut y;
    bar(&y);
    baz(&mut y);
    baz(z);
}

// move
fn foo(_foobar: Foobar) {
}

// read only reference
fn bar(_foobar: &Foobar) {
}

// mutable reference
fn baz(_foobar: &mut Foobar) {
}

可变引用与可变变量

最开始没有解释 x 之前的 mut:

fn main() {
    let mut x: Foobar = Foobar(1);
    let y: &mut Foobar = &mut x;
    println!("Before uses_foobar");
    uses_foobar(&x);
    uses_foobar(y);
    println!("After uses_foobar");
}

默认情况下,变量是不可变的,因此不允许任何形式的突变。 您不能对不可变变量进行可变引用,因此x必须标记为可变。 这是一种更简单的方法:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let mut x = Foobar(1);

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

如果您删除了 mut,这将失败。

转移到可变

这让我很困扰,我想这会让其他的 Haskellers 感到困扰。正如刚才提到的,下面的代码不会编译通过:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

显然你不能改变 x,但是让我们稍微改变一下:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x = Foobar(1);
    foo(x);
}

fn foo(mut x: Foobar) {

    x.0 = 2; // changes the 0th value inside the product

    println!("{:?}", x);
}

在学习 Rust 之前,我会反对这一点: x 是不可变的,因此我们不应该允许将它传递给一个需要可变 x 的函数。 这里的可变性是变量的一个特性,而不是值本身。 当你把 x 移动到 foo 中,main 不再有权限访问 x,也不在乎它是否变异了。 在 foo 中,我们已经明确指出 x 可以变异,所以我们很酷。

这与 Haskell 的观点大相径庭。

Copy trait

上一次我们讨论这个主题时,使用了数值类型vs字符串。让我们再努力一点,下面的代码是否编译?

fn uses_i32(i: i32) {
    println!("I consumed an i32: {}", i);
}

fn main() {
    let x = 1;
    uses_i32(x);
    uses_i32(x);
}

它不能工作,对吧?x 被移动到 uses_i32,然后再次使用。不过,它编译得很好!为什么? Rust有一个特殊的Trait:Copy。它表示一种非常廉价类型,可以自动按值传递。 这正是i32的情况。 如果需要,可以使用 “克隆” Trait显式地执行此操作:

#[derive(Debug, Clone)]
struct Foobar(i32);

impl Drop for Foobar {
    fn drop(&mut self) {
        println!("Dropping: {:?}", self);
    }
}

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x.clone());
    uses_foobar(x);
}

为什么我们不需要在第二个 uses_foobar 时使用 x.clone()?

练习3

更改下面代码,而无需完全修改 main 函数,就可以让它成功地编译和运行。 一些提示:Debug 是可以自动派生的特殊trait,为了获得 Copy 实现,您还需要一个 Clone 实现。

#[derive(Debug)]
struct Foobar(i32);

fn uses_foobar(foobar: Foobar) {
    println!("I consumed a Foobar: {:?}", foobar);
}

fn main() {
    let x = Foobar(1);
    uses_foobar(x);
    uses_foobar(x);
}

生命周期

与所有权最相关的术语是生命周期。 每个值都必须拥有,并且在 dropped 之前是一直拥有。 到目前为止,我们研究的所有内容都涉及隐式生命周期。 但是,随着代码变得越来越复杂,我们需要更加明确地说明这些生命周期。 我们将再讨论一次。

练习4

添加一个 double 函数的实现,让这段代码编译、运行并输出数字2:

#[derive(Debug)]
struct Foobar(i32);

fn main() {
    let x: Foobar = Foobar(1);
    let y: Foobar = double(x);
    println!("{}", y.0);
}

注意: 要提供函数的返回值,在参数列表后面放置: -> ReturnType。

结构体和枚举

我在上面提到过,struct Foobar (i32) 是包裹 i32 的一个新类型。 这实际上是更一般的结构体的特殊情况,其中可以有0个或多个字段,以其数值位置命名。 位置开始编号为0,上帝 Linus Torvalds 的打算。

这有一些例子:

struct NoFields; // 可能看起来很奇怪,我们以后可能会讲到这个例子
struct OneField(i32);
struct TwoFields(i32, char);

您还可以使用记录语法:

struct Person {
    name: String,
    age: u32,
}

结构被称为产品类型,这意味着它们包含多个值。 Rust还提供枚举,即枚举类型或带标签的联合。 这些是替代方法,您可以在其中选择一个选项。 一个简单的枚举将是:

enum Color {
    Red,
    Blue,
    Green,
}

但是枚举变量也可以取值:

enum Shape {
    Square(u32), // size of one side
    Rectangle(u32, u32), // width and height
    Circle(u32), // radius
}

Bouncy

别说了,我们开始吧! 我想创建一个弹跳球的模拟。

让我们一起逐步完成创建这样一个游戏的过程。在课程结束时,我将提供完整的 src/main.rs。 但是强烈建议你和我一起在下面的章节中实现这一点。 尽量避免复制粘贴,而是自己键入代码,使 Rust 语法更加舒适。

初始化项目

这部分很简单:

$ cargo new bouncy

如果你进入 bouncy 这个目录并运行 cargo run,你会得到如下输出:

$ cargo run
   Compiling bouncy v0.1.0 (/Users/michael/Desktop/bouncy)
    Finished dev [unoptimized + debuginfo] target(s) in 1.37s
     Running `target/debug/bouncy`
Hello, world!

我们今天唯一触及的文件是 src/main.rs,它将包含我们程序的源代码。

定义数据结构

要跟踪屏幕上弹跳的球,我们需要了解以下信息:

  • 包含球的盒子的宽度

  • 包含球的盒子的高度

  • 球的 x 和 y 坐标

  • 球的垂直方向(向上或向下)

  • 球的水平方向(左或右)

我们将定义新的数据类型来跟踪垂直和水平方向,并使用 u32 来跟踪位置。

我们可以将 VertDir 定义为枚举。 这是枚举可以处理的简化版本,因为我们没有给它任何有效负载。 我们稍后会做更复杂的东西。

enum VertDir {
    Up,
    Down,
}

继续并定义一个 HorizDir,它可以跟踪我们向左还是向右移动。 现在,要跟踪一个球,我们需要知道其 x 和 y 位置以及其垂直和水平方向。 这将是一个结构,因为我们要跟踪多个值,而不是(例如枚举)在不同选项之间进行选择。

struct Ball {
    x: u32,
    y: u32,
    vert_dir: VertDir,
    horiz_dir: HorizDir,
}

定义一个 Frame 结构,用于跟踪游戏区域的宽度和高度。 然后用一个 Game struct 把它们联系起来:

struct Game {
    frame: Frame,
    ball: Ball,
}

创建一个新的 Game

我们可以在 Game 类型本身上定义一个方法来创建一个新的游戏。 我们将分配一些默认的宽度和高度以及初始的球位置。

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

重写此实现以避免使用任何 let 语句。

我们如何使用 VertDir::Up。默认情况下,Up 构造函数不会导入到当前名称空间中。 另外,由于局部变量名称与字段名称相同,因此我们可以使用 frame,ball 而不是 frame: frame, ball:ball 来定义游戏。

弹起

让我们实现一个球从墙上反弹的逻辑,写出逻辑:

  • 如果 x 值是0,在边框的左边,因此我们应该向右移动。

  • 如果 y = 0,向下移动。

  • 如果 x 比边框的宽度小1,我们应该向左移动。

  • 如果 y 比框架的高度小1,我们应该向上移动。

  • 否则,我们应该继续朝着同一个方向前进。

我们需要修改 Ball,并以边框作为参数。 我们将在 Ball 类型上实现这个方法:

impl Ball {
    fn bounce(&mut self, frame: &Frame) {
        if self.x == 0 {
            self.horiz_dir = HorizDir::Right;
        } else if self.x == frame.width - 1 {
            self.horiz_dir = HorizDir::Left;
        }

        ...

自己继续实现这个函数的其余部分。

移动

一旦我们通过调用 bounce 知道了球的移动方向,我们就可以将球移动一个位置。 我们将在 impl Ball 中添加这个方法:

fn mv(&mut self) {
    match self.horiz_dir {
        HorizDir::Left => self.x -= 1,
        HorizDir::Right => self.x += 1,
    }
    ...
}

还要实现其中的垂直部分。

步骤

我们需要向 Game 添加方法来执行游戏的步骤。 这将涉及的弹跳和移动包含在 impl Game 中:

fn step(&self) {
    self.ball.bounce(self.frame);
    self.ball.mv();
}

在这个实现中有一些 bug 需要修复。

渲染

我们需要能够显示游戏的完整状态。我们会看到这个初始实现有它的缺陷,,但是我们要打印整个网格。 我们将添加边框,使用字母 o 表示球,并在边框内的所有其他区域放置空格。 为此,我们将使用 Display trait。

让我们把一些类型放到我们的名称空间中,在源文件的顶部添加:

use std::fmt::{Display, Formatter};

现在,让我们确保类型签名是正确的:

impl Display for Game {
    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
        unimplemented!()
    }
}

在实现函数之前,可以使用 unimplemented!() 宏对函数进行打桩。 最后,让我们完成 main 函数,该函数将打印初始游戏:

fn main () {
    println!("{}", Game::new());
}

如果一切都设置正确,运行 cargo run 将导致 “not yet implemented” 的 panic。 如果您遇到编译错误,现在就去修复它。

顶端边框

好了,现在我们可以实现fmt了。 首先,让我们绘制顶部边框。 边框的组成将是一个加号,一系列破折号(基于框架的宽度),一个加号和换行符。 我们将使用 write! 宏、 range语法 (low. . high) 和 for 循环:

write!(fmt, "+");
for _ in 0..self.frame.width {
    write!(fmt, "-");
}
write!(fmt, "+\n");

看起来不错,但是我们得到一个编译错误:

error[E0308]: mismatched types
  --> src/main.rs:79:60
   |
79 |       fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
   |  ____________________________________________________________^
80 | |         write!(fmt, "+");
81 | |         for _ in 0..self.frame.width {
82 | |             write!(fmt, "-");
83 | |         }
84 | |         write!(fmt, "+\n");
   | |                           - help: consider removing this semicolon
85 | |     }
   | |_____^ expected enum `std::result::Result`, found ()
   |
   = note: expected type `std::result::Result<(), std::fmt::Error>`
              found type `()`

上面写着 “considering removing this semicolon”。 记住,放置分号将迫使我们的语句返回 () ,但我们需要一个Result值。 而且好像 write! 宏为我们提供了一个Result值。 果然,如果我们删除结尾的分号,我们将得到一些有用的东西:

    Finished dev [unoptimized + debuginfo] target(s) in 0.55s
     Running `target/debug/bouncy`
+------------------------------------------------------------+

您可能会问:来自其他调用 write! 的所有 Result 值呢? 好问题! 我们稍后再讨论。

底端边框

顶部和底部边框完全相同。 我们不要复制代码,而是定义一个可以调用两次的闭包。 我们用语法 | args | { body }在 Rust 中引入一个闭包。 这个闭包不带参数,所以看起来是这样的:

let top_bottom = || {
    write!(fmt, "+");
    for _ in 0..self.frame.width {
        write!(fmt, "-");
    }
    write!(fmt, "+\n");
};

top_bottom();
top_bottom();

首先,我们将再次得到一个关于 Result 和()的错误。 你需要删除两个分号来解决这个问题。 一旦您完成了这些操作,您将得到一个全新的错误消息。 耶!

error[E0596]: cannot borrow `top_bottom` as mutable, as it is not declared as mutable
  --> src/main.rs:88:9
   |
80 |         let top_bottom = || {
   |             ---------- help: consider changing this to be mutable: `mut top_bottom`
...
88 |         top_bottom();
   |         ^^^^^^^^^^ cannot borrow as mutable

错误消息告诉我们确切的操作:在 let top_bottom 中间插入一个 mut。 这样做,并确保它可以解决问题。 现在的问题是:为什么? top_bottom闭包已从环境中捕获了 fmt 变量。 为了使用它,我们需要调用 write! 宏,该变量会更改该 fmt 变量。 因此,每次对 top_bottom 的调用本身都是一个突变。 因此,我们需要将 top_bottom 标记为可变的。

有三种不同类型的封闭性trait: Fn、 FnOnce 和 FnMut。 我们将在后面的教程中讨论它们之间的区别。

无论如何,我们现在应该有一个顶部和底部边界在我们的输出。

行数

让我们打印每一行。 在两次 top_bottom() 调用之间,我们将保留一个for循环:

for row in 0..self.frame.height {
}

在这个循环中,我们需要添加左边框和右边框:

write!(fmt, "|");
// more code will go here
write!(fmt, "|\n");

继续使用 cargo run 运行程序,你会有一个不愉快的惊喜:

error[E0501]: cannot borrow `*fmt` as mutable because previous closure requires unique access
  --> src/main.rs:91:20
   |
80 |         let mut top_bottom = || {
   |                              -- closure construction occurs here
81 |             write!(fmt, "+");
   |                    --- first borrow occurs due to use of `fmt` in closure
...
91 |             write!(fmt, "|");
   |                    ^^^ borrow occurs here
...
96 |         top_bottom()
   |         ---------- first borrow used here, in later iteration of loop

哦,不,我们将不得不处理借用检查!

与借用检查员斗争

好吧,还记得 top_bottom 闭包捕获了对 fmt 的可变引用吗? 好吧,这现在给我们带来了麻烦。 一次只能有一个可变引用在起作用,并且top_bottom 在我们方法的整个主体中都将其保留。 在这种情况下,有一个简单的解决方法:将fmt用作闭包的参数,而不是捕获它:

let top_bottom = |fmt: &mut Formatter| {

修复 top _ bottom 的调用,您应该得到如下输出(删除了一些额外的行)。

+------------------------------------------------------------+
||
||
||
||
...
+------------------------------------------------------------+

好了,现在我们可以回到…

列数

还记得 // more code will go here 注释吗? 是时候换掉它了! 我们将为每个列使用另一个 for 循环:

for column in 0..self.frame.width {
    write!(fmt, " ");
}

运行 cargo run 会显示一个完整的框架,不错! 不幸的是,它不包括我们的球。 当列与球的 x 相同时,我们需要写一个 o 字符,而不是空格,对于 y 也是一样的:

let c = if row == self.ball.y {
    'o'
} else {
    ' '
};
write!(fmt, "{}", c);

输出有些问题(cargo run完成测试)。修复它,你的渲染功能将完成!

无限循环

我们几乎完成了! 我们还需要在主函数中添加一个无限循环:

  • 打印游戏

  • 游戏步骤

  • 休眠一会儿

我们的目标是 30FPS,所以我们要睡眠 33 毫秒。 但是在 Rust 中我们如何入睡呢? 为了解决这个问题,让我们去 Rust standard Library 文档中搜索 sleep。 第一个结果是 std::thread::sleep,这似乎是个不错的选择。 查看那里的文档,特别是那个很棒的例子,来理解这段代码。

fn main () {
    let game = Game::new();
    let sleep_duration = std::time::Duration::from_millis(33);
    loop {
        println!("{}", game);
        game.step();
        std::thread::sleep(sleep_duration);
    }
}

这段代码中有一个编译错误。 试着预测它是什么。 如果你不能解决它,问编译器,然后修复它。 运行 cargo run 应该会成功显示你一个弹跳球。

问题

在这个实现中,我关心两个问题:

  • 输出可能有点抖动,特别是在慢速终端上。 我们实际上应该使用类似 curses 库的东西来处理输出的双缓冲。

  • 如果您之前运行过 cargo run,则可能看不到。 运行cargo clean 和 cargo build 以强制进行重建,您应该看到以下警告:

warning: unused `std::result::Result` which must be used
  --> src/main.rs:88:9
   |
88 |         top_bottom(fmt);
   |         ^^^^^^^^^^^^^^^^
   |
   = note: this `Result` may be an `Err` variant, which should be handled

我在上面提到过这个问题: 我们忽略了来自调用 write! 宏带来的失败!,但是使用分号丢弃 Result。 这个问题有一个很好的单字符解决方案。 这为正确处理 Rust 中的错误提供了基础。 不过,我们会把这个留到下次再说。 现在,我们只需要忽略这个警告。

完整源码

您可以在 Github 上找到这个实现的完整源代码。 提醒: 如果您逐步完成上面的代码并自己实现它,那将会更好。

在 top-bottom 调用的最后,我添加了一个我们在教程中还没有涉及到的语法。 下一节我们将更详细地讨论这个问题。

最后更新于