通过 Advent Of Code 学习 Rust - 第一部分

2025-06-08

通过 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");
}
Enter fullscreen mode Exit fullscreen mode

让我们逐行看一下:

let- let 关键字的主要用途是在 let 语句中,用于将一组新的变量引入当前作用域,这些变量由模式指定。模式通常是一个变量,这意味着不进行模式匹配,并且给定的表达式将绑定到该变量。除此之外,let 绑定中使用的模式可以根据需要设置得尽可能复杂,前提是模式足够详尽。

附注:Rust 中的所有变量都是不可变的,mut需要明确写入关键字才能使变量可变。

语法可以遵循以下模式:

let <identifier>: type = expr

其中类型表示此变量可以具有的一组值,受其类型限制,expr可以是值,也可以是计算结果为类型值的表达式type

let args: Vec<String> = env::args().collect();
let filename = &args[1];
Enter fullscreen mode Exit fullscreen mode

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]`
Enter fullscreen mode Exit fullscreen mode

我们可以看到,发生了一次移动,从内存位置内的移动意义上来说,我们强制尝试将 args 向量位置 1 上的值提取到 filename 变量中,但这是行不通的。

相反,我们可以借用该值:我们可以引用它,而无需真正“拥有”它,并且它的所有权仍然属于向量或参数。通过这样做,我们不需要移动它或复制它,然后我们可以将该变量filename作为向量第一个元素的别名来引用。

请注意,我们使用索引 1 而不是 0,因为第一个参数通常是可执行文件的路径。

如果我们创建一个名为:的文件,test.txt其内容如下:

1
2
3
Enter fullscreen mode Exit fullscreen mode

并将其放在我们程序的同一目录中,然后我们可以扩展我们的程序以将文件的内容作为字符串打印出来:

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);
}
Enter fullscreen mode Exit fullscreen mode

这将打印:

In file: test.txt
"1\n2\n3\n"
Enter fullscreen mode Exit fullscreen mode

要编译 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);
}
Enter fullscreen mode Exit fullscreen mode

我们学习了如何声明变量、可变变量(通过文档了解 mut 关键字)、如何将文件内容读取为字符串、如何对这些值应用类型和值转换、如何将结果打印到控制台,以及如何编译和运行一个简单的 Rust 程序!

就像刚开始学习一门新语言时经常出现的情况一样,这种简单的程序具有很大的附加值。

随着时间的推移,通过解决新问题,我的理解会不断加深,我的 Rust 代码质量也会不断提升。敬请期待!

祝大家圣诞快乐!!!

鏂囩珷鏉ユ簮锛�https://dev.to/brunooliveira/learning-rust-via-the-advent-of-code-part-1-23le
PREV
NodeJS 项目中的 PeerDependencies 是什么?
NEXT
必不可少的付费开发者工具?GenAI LIVE!| 2025 年 6 月 4 日