速成课第2节
可以说 Rust 与其他流行编程语言最大的区别在于它的所有权模型。 在本课中,我们将接触 Rust 的所有权基础知识。 你可以在 Rust Book 中学习到更多关于所有权的章节。
这篇文章是基于 FP 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在介绍文章的顶部找到这个系列中所有文章的链接。 也可订阅 RSS 频道。
格式
我将尝试一些课程格式。 我想涵盖以下两个方面:
关于所有权更多理论讨论
尝试实际编写程序
随着时间的推移,我打算花更多的时间在后者上,而不是前者,尽管我们现在仍然需要花大量的时间在前者上。 我将在这篇文章的开头讨论所有权,然后我们将实现第一个版本的 “弹跳球“。
如果这种方法对大家有效的话,可能会让大家觉得有点拘谨,反馈也是值得赞赏的。
与 Haskell 的比较
我将首先比较 Rust 和 Haskell,因为两种语言都有一个强大的不变性概念。 然而,Haskell 是一种垃圾回收语言。 让我们来看看这两种语言的比较。在 Haskell 中:
默认情况下一切都是不可变的
您可以使用显式的可变包装器(如
IORef
或MVar
)来标记可变性您可以随意对数据的引用进行共享
垃圾回收不确定地释放内存
当您需要确定性的资源处理(例如文件句柄)时,需要使用方括号模式或类似的模式
在 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 调用的最后,我添加了一个我们在教程中还没有涉及到的语法。 下一节我们将更详细地讨论这个问题。
最后更新于
这有帮助吗?