# 速成课第2节

可以说 Rust 与其他流行编程语言最大的区别在于它的所有权模型。 在本课中，我们将接触 Rust 的所有权基础知识。 你可以在 Rust Book 中学习到[更多关于所有权的章节](https://doc.rust-lang.org/book/second-edition/ch04-01-what-is-ownership.html)。

这篇文章是基于 [FP](https://www.fpcomplete.com/rust) 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章，你可以在[介绍文章的顶部](https://www.snoyman.com/blog/2018/10/introducing-rust-crash-course)找到这个系列中所有文章的链接。 也可[订阅 RSS](https://www.snoyman.com/feed/rust-crash-course) 频道。

## 格式

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

* 关于所有权更多理论讨论
* 尝试实际编写程序

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

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

## 与 Haskell 的比较

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

* 默认情况下一切都是不可变的
* 您可以使用显式的可变包装器(如`IORef` 或 `MVar`)来标记可变性
* 您可以随意对数据的引用进行共享
* 垃圾回收不确定地释放内存
* 当您需要确定性的资源处理（例如文件句柄）时，需要使用[方括号模式](https://www.snoyman.com/blog/2018/10/raii-better-than-bracket-pattern)或类似的模式

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

所有权从以下规则开始：

* 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 将会是一个错误：

```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);
    uses_foobar(x);
}
```

结果如下:

```rust
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 实现来说明这一点：

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

```rust
#[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");
}
```

输出：

```rust
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 的很好的解释](https://stackoverflow.com/questions/50251487/what-are-non-lexical-lifetimes))。 我们可以证明当前作用域的性质：

```rust
#[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");
}
```

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

这个程序的输出是：

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

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

## Borrows / references

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

```rust
#[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 的不可变引用“
* &#x20;use\_foobar 内部，我们不需要显式地取消引用 foobar 值，这是由 Rust 自动完成的
* 在 main 中，我们现在可以使用 x 来调用 uses\_foobar 两次
* 为了从值创建一个引用，我们在变量前面使用 &&#x20;

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

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

### 练习2

我们也希望能够为 uses\_foobar 使用对象语法。 在 Foobar 类型上创建方法 use\_it，以打印 "I consumed" 的信息。

&#x20;*提示： 你需要在 impl Foobar { ... }里面操作。*

## 多存活引用

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

```rust
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 吗：

```rust
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");
}
```

这将导致错误消息：

```rust
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 不允许以任何其他方式同时引用和访问值。

```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在类型级别跟踪可变性。 好极了！

## 挑战

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

```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：

```rust
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必须标记为可变。 这是一种更简单的方法：

```rust
#[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 感到困扰。正如刚才提到的，下面的代码不会编译通过：

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

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

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

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

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

```rust
#[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字符串。让我们再努力一点，下面的代码是否编译？

```rust
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显式地执行此操作：

```rust
#[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 实现。

```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);
    uses_foobar(x);
}
```

## 生命周期

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

## 练习4

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

```rust
#[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 的打算。

这有一些例子：

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

您还可以使用记录语法：

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

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

```rust
enum Color {
    Red,
    Blue,
    Green,
}
```

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

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

## Bouncy

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

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

### 初始化项目

这部分很简单：

```rust
$ cargo new bouncy
```

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

```rust
$ 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 定义为枚举。 这是枚举可以处理的简化版本，因为我们没有给它任何有效负载。 我们稍后会做更复杂的东西。

```rust
enum VertDir {
    Up,
    Down,
}
```

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

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

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

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

### 创建一个新的 Game

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

```rust
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 类型上实现这个方法：

```rust
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 中添加这个方法：

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

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

### 步骤

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

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

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

### 渲染

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

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

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

现在，让我们确保类型签名是正确的：

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

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

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

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

### 顶端边框

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

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

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

```rust
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值。 果然，如果我们删除结尾的分号，我们将得到一些有用的东西：

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

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

### 底端边框

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

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

top_bottom();
top_bottom();
```

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

```rust
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循环：

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

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

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

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

```rust
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用作闭包的参数，而不是捕获它：

```rust
let top_bottom = |fmt: &mut Formatter| {
```

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

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

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

### 列数

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

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

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

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

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

### 无限循环

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

* 打印游戏
* 游戏步骤
* 休眠一会儿

我们的目标是 30FPS，所以我们要睡眠 33 毫秒。 但是在 Rust 中我们如何入睡呢？ 为了解决这个问题，让我们去[ Rust standard Library](https://doc.rust-lang.org/std/index.html) 文档中[搜索 sleep](https://doc.rust-lang.org/std/index.html?search=sleep)。 第一个结果是 [std::thread::sleep](https://doc.rust-lang.org/std/thread/fn.sleep.html)，这似乎是个不错的选择。 查看那里的文档，特别是那个很棒的例子，来理解这段代码。

```rust
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 以强制进行重建，您应该看到以下警告：

```rust
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 上找到这个实现的[完整源代码](https://gist.github.com/snoyberg/5307d493750d7b48c1c5281961bc31d0)。 提醒: 如果您逐步完成上面的代码并自己实现它，那将会更好。

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://chapin666.gitbook.io/rust-crash-course-zh/2.-suo-you-quan-ji-chu/su-cheng-ban-di-2-ke.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
