上一次,我们完成了一个弹跳球实现,但带来了一些缺点: 缺乏亮点的错误处理和难看的缓冲。 它还有另一个限制: 静态帧大小。 今天,我们将解决所有这些问题,从最后一个开始: 让我们获得一些命令行参数来控制帧大小。
这篇文章是基于 完成 Rust 教学系列的一部分。 如果你在博客之外阅读这篇文章,你可以在 找到这个系列中所有文章的链接。 也可 频道。
就像上次一样,我希望你,作为读者,和我一起修改源代码。 一定要在阅读的时候编写代码!
命令行参数
我们将修改我们的应用程序如下:
接受两个命令行参数: width 和 height
听起来很简单。 在实际的应用程序中,我们会使用一个适当的参数处理库,比如 。 但是现在,我们正处于初级水平。 就像我们对 sleep 函数所做的一样,让我们从 中搜索 args 这个词开始。 前两个条目看起来都很相关。
std::env::Args
遍历进程参数的迭代器,为每个参数生成一个 String 值。
std::env::args
返回该程序启动时使用的参数(通常通过命令行传递)。
按照惯例,现在是提出这个问题的好时机:
模块名称(如 std 和 env)和函数名称(如 args)使用 snake_cased 格式
std 模块有一个 env 模块。 env 模块有一个 Args 类型和一个 Args 函数。 为什么我们两者都需要? 更奇怪的是,让我们来看看 args 函数的类型签名:
数据类型模式
也许在 Rust 中有一个合适的术语来形容这个,但是我自己还没有见过。 (如果有的话,请让我知道,这样我才能使用恰当的术语)。 在 Rust 生态系统中有一个普遍的模式,根据我的经验,这个模式从迭代器开始,然后继续到更高级的主题,如 futures 和 async i/o。
因此,我们定义了许多辅助数据类型,允许编译器执行一些优化
我们将 traits 定义为一个接口,使这些类型能够很好地组合在一起
听起来很抽象? 不用担心,我们稍后会把它具体化。以下是所有这些的实际结果:
我们最终针对 traits 编写了相当多的程序,traits 提供了一个通用的抽象和许多辅助函数
好了,解决了这个问题,让我们回到命令行参数!
使用迭代器对 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);
}
}
注意:在大多数 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);
}
}
正如上面提到的,这种数据类型分层的方法对性能非常有利。 大量使用迭代器的代码通常会编译成一个高效的循环,可与您手动编写的最佳循环相媲美。 但是,迭代器代码级别更高,更具声明性,易于维护和扩展。
关于迭代器还有很多内容,但是我们暂时就此打住,因为我们仍然需要处理我们的命令行参数,而且我们需要先学习另外一件问题。
解析整数
好吧,你回来了? 涡轮鱼 是个有趣的名字,对吧?
尝试编写一个程序,将解析每个命令行参数的结果输出为 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)
这是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());
}
神圣的嵌套地狱,这是一个很大的缩进! 模式是非常简单的:
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 函数:
所有这些都需要返回 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
复制 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。让我们开始一个新的空项目:
复制 $ curl https://gist.githubusercontent.com/snoyberg/5307d493750d7b48c1c5281961bc31d0/raw/8f467e87f69a197095bda096cbbb71d8d813b1d7/main.rs > src/main.rs
运行 cargo Run 并确保它工作正常。您可以使用 Ctrl-C 杀死该程序。
我们已经在上面编写了完全可用的参数解析代码。 与其将其放在同一个源文件中,不如将其放在自己的文件中。 为此,我们将不得不使用Rust中的模块。
复制 $ 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 来实现。 将以下行添加到文件顶部:
如果您之前输入了无效行,那么运行 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,即:
让我们运行这个程序:
复制 $ 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。 最后,如果您运行 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
然而,我们还有一个选择: 我们可以直接从 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! 的做法,这是一个好习惯。
下一节
到目前为止,仍然没有解决双缓冲问题,我们将在下次讨论。 我们还将从标准库中引入更多的错误处理。 也许可以在迭代器方面做得更多一些。