通过 Advent Of Code 学习 Rust - 第一部分
动机
我决定在明年学习 Rust,我将首先解决Advent of Code的问题,这是一个在 reddit 上流行的学习新语言的网站,因此它非常适合这项任务。
我将遵循 Rust 官方文档,并尝试改进我的代码风格,从 Rust 零基础到成为 Rust 大师。
让我们看一下问题陈述。
问题
以下是 Advent Of Code 问题 1 的陈述:
圣诞老人在给其他星球派送礼物时被困在太阳系边缘!为了准确计算他在太空中的位置,安全调整他的曲速引擎,并及时返回地球拯救圣诞节,他需要你为他带来 50 颗星星的测量数据。
通过解决谜题收集星星。降临节日历中每天都会提供两个谜题;完成第一个谜题后,第二个谜题就会解锁。每个谜题都会奖励一颗星星。祝你好运!
精灵们会迅速将你装上宇宙飞船,准备发射。
在第一次“继续/不继续”投票中,每个精灵都表示“继续”,直到燃料计数器显示“上层燃料”。他们还没有确定所需的燃料量。
发射特定模块所需的燃料取决于其质量。具体来说,要计算某个模块所需的燃料,取其质量,除以 3,向下取整,然后减 2。
例如:
如果质量为 12,除以 3 并向下取整得到 4,然后减去 2 得到 2。
如果质量为 14,除以 3 并向下取整仍然得到 4,因此所需燃料也是 2。
如果质量为 1969,则所需燃料为 654。
如果质量为 100756,则所需燃料为 33583。
燃料计数器需要知道总燃料需求。要计算总燃料需求,请分别计算每个模块质量(您的谜题输入)所需的燃料,然后将所有燃料值加在一起。
您的航天器上所有模块的燃料需求总和是多少?
我们可以看到,问题本身非常简单:
我们必须从文件中读取数字,对每个数字执行一些转换操作,然后将所有这些值相加。
然而,对于这样一个简单的问题,已经有相当多的内容需要解开,让我们看看:
- 我们需要读取一个输入文件;
- 我们需要能够将其内容解释为“整数行”;
- 然后我们需要转换每个数字,并保持转换值的运行总和直到文件结束,然后将其输出作为我们的答案;
让我们更详细地了解每个单独的操作。
读取输入文件
读取输入文件是解决这个问题的第一步。
使用“read file in rust”进行 Google 搜索,我们会找到官方文档,我们可以从中获取并调整以下代码:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let filename = &args[1];
println!("In file {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
}
让我们逐行看一下:
let
- let 关键字的主要用途是在 let 语句中,用于将一组新的变量引入当前作用域,这些变量由模式指定。模式通常是一个变量,这意味着不进行模式匹配,并且给定的表达式将绑定到该变量。除此之外,let 绑定中使用的模式可以根据需要设置得尽可能复杂,前提是模式足够详尽。
附注:Rust 中的所有变量都是不可变的,mut
需要明确写入关键字才能使变量可变。
语法可以遵循以下模式:
let <identifier>: type = expr
其中类型表示此变量可以具有的一组值,受其类型限制,expr
可以是值,也可以是计算结果为类型值的表达式type
。
let args: Vec<String> = env::args().collect();
let filename = &args[1];
args
这些行将类型为(字符串向量)的变量的值设置Vec<String>
为传递给我们程序的命令行参数的值。
env::args()
,返回一个迭代器,然后将其collect()
放入一个字符串向量中,其中包含传递给我们程序的每个命令行参数。
然后,我们创建一个新变量 filename,它将包含我们的输入文件的名称。
请注意,这是在访问向量之前使用“与”符号完成的。
这些符号是引用,它们允许您引用某个值但不拥有它的所有权。
因为我们使用的是字符串,它们的工作方式与其他变量不同(不仅在 Rust 中,在许多其他语言中也是如此),默认情况下它们是不可变的,这意味着在将字符串分配给新变量时,字符串不会被复制,而是对字符串的引用会被更新。实际上,如果我们从上面的示例中删除“与”符号,我们将收到以下错误:
let filename = args[1];
| ^^^^^^^
| |
| move occurs because value has type `std::string::String`, which does not implement the `Copy` trait
| help: consider borrowing here: `&args[1]`
我们可以看到,发生了一次移动,从内存位置内的移动意义上来说,我们强制尝试将 args 向量位置 1 上的值提取到 filename 变量中,但这是行不通的。
相反,我们可以借用该值:我们可以引用它,而无需真正“拥有”它,并且它的所有权仍然属于向量或参数。通过这样做,我们不需要移动它或复制它,然后我们可以将该变量filename
作为向量第一个元素的别名来引用。
请注意,我们使用索引 1 而不是 0,因为第一个参数通常是可执行文件的路径。
如果我们创建一个名为:的文件,test.txt
其内容如下:
1
2
3
并将其放在我们程序的同一目录中,然后我们可以扩展我们的程序以将文件的内容作为字符串打印出来:
fn main() {
let args: Vec<String> = env::args().collect();
let filename = &args[1];
println!("In file: {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
println!("{:?}", contents);
}
这将打印:
In file: test.txt
"1\n2\n3\n"
要编译 Rust 程序,我们可以使用 Rust 编译器rustc
:
rustc main.rs
这将生成一个名为 main 的可执行文件,然后我们可以按如下方式执行它:
./main test.txt
请注意,这不是管理大型 Rust 项目的最佳方式,因为我们有一个名为 cargo 的构建管理器系统,它更适合大型项目,并且有助于编译和管理依赖项,但就目前而言,这样就可以了。
这行代码println!("{:?}", contents);
只是将文件内容以字符串形式打印出来。这"{:?}"
是一个显示格式化程序,用于告诉 Rust 如何将某些值打印到控制台。
将文件内容解释为数字
为了对这些值求和,我们需要将它们视为数字,而不是字符串。
为此,我们可以将字符串解析为数字,方法是用换行符分割字符串,然后解析每个值。我们使用以下代码来实现:
let v: Vec<i32> = contents.split("\n").filter_map(|w| w.parse().ok()).collect();
让我们剖析一下:
我们首先用换行符分割文件内容(一个字符串),这样我们基本上得到了一个字符串迭代器。
然后我们将该filter_map
函数应用到它,函数如下:
|w| w.parse().ok()
参数位于|
字符之间,其后跟函数表达式,可以读懂并理解为简单的英语,我们尝试将某个单词 w 解析为一个整数,我们将过滤掉解析失败的单词,保留解析成功的单词ok()
。
之后,我们将解析的值收集到一个整数向量中(在 Rust 中用 表示i32
)。
这就是如何将文件内容解释为整数。
让我们完成它并对转换后的值求和。
转换并求和值
为了转换值,我们可以应用执行问题要求的映射操作,映射后将值转换为整数:
let v: Vec<i32> = contents.split("\n").filter_map(|w| w.parse().ok()).map(|x: i32| x/3 - 2).collect();
最后,为了得到问题的答案,我们需要创建一个新变量来保存结果的值:
let tot: i32 = v.iter().sum();
我们首先需要使用获取向量上的迭代器iter()
,然后我们可以调用它的内置sum()
函数来获取我们想要的值。
最终程序
完整的程序如下所示:
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let filename = &args[1];
println!("In file: {}", filename);
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
let v: Vec<i32> = contents.split("\n").filter_map(|w| w.parse().ok()).map(|x: i32| x/3 - 2).collect();
let tot: i32 = v.iter().sum();
println!("{:?}", tot);
}
我们学习了如何声明变量、可变变量(通过文档了解 mut 关键字)、如何将文件内容读取为字符串、如何对这些值应用类型和值转换、如何将结果打印到控制台,以及如何编译和运行一个简单的 Rust 程序!
就像刚开始学习一门新语言时经常出现的情况一样,这种简单的程序具有很大的附加值。
随着时间的推移,通过解决新问题,我的理解会不断加深,我的 Rust 代码质量也会不断提升。敬请期待!
祝大家圣诞快乐!!!
鏂囩珷鏉ユ簮锛�https://dev.to/brunooliveira/learning-rust-via-the-advent-of-code-part-1-23le