原文链接:www.freecodecamp.org/news/how-to… 作者:Claudio Restifo
自2015年首次开源发布以来,Rust编程语言已经获得了社区的大量关注。自2016年以来,它也被评为Stack Overflow开发者调查中最受欢迎的编程语言。
Rust是由Mozilla设计的,被认为是一种系统编程语言(如C或C++)。它没有垃圾收集器,这使得它的性能真的很好。但它的设计往往让它看起来和感觉都非常“高级”。
Rust的学习曲线被认为有些陡峭。我自己不是这门语言的大师,但是通过本教程,我将尝试给你一个实用的方法来帮助你更深入地理解一些概念。
我们将在这个实践教程中构建什么
我决定遵循JavaScript应用程序的悠久传统,制作一个待办事项应用程序作为我们的第一个项目。我们将使用命令行,所以熟悉它是必要的。您还需要一些一般编程概念的知识。
此应用程序将在终端中运行。我们将值存储为项目的集合和表示其活动状态的布尔值。
我们将在这里涵盖的
-
Rust中的错误处理。
-
选项和Null类型。
-
结构和impl.
-
终端I/O.
-
文件系统处理。
-
Rust中的所有权和借款。
-
匹配模式。
-
迭代器和闭包。
-
使用外部板条箱。
在我们开始之前
在我们开始之前,来自JavaScript背景的人给了我们一些建议:
-
Rust是一种强类型语言。这意味着,当编译器无法为我们推断类型时,我们将不得不处理变量类型。
-
与JavaScript相反,这里没有AFI。这意味着我们必须自己键入分号(";"),除非它是函数的最后一个语句。在这种情况下,你可以省略;把它作为回报。
废话不多说,我们开始吧。
如何开始使用 Rust
要开始,请将Rust下载到您的计算机上。要做到这一点,请按照您在Rust官方网站的入门页面上找到的说明进行操作。
在那里,您还可以找到将语言与您最喜欢的编辑器集成的说明,以获得更好的体验。
除了Rust编译器本身,Rust还附带了一个名为Cargo的工具。Cargo是Rust包管理器,对JavaScript开发人员来说,它就像npm或纱线一样。
要启动新项目,请导航到您希望创建项目的位置,然后只需运行货运新。在我的情况下,我决定将我的项目命名为to do-cli,这样我就可以运行:
$ cargo new todo-cli
现在导航到新创建的目录并列出其内容。你应该在里面看到两个文件:
$ tree .
.
├── Cargo.toml
└── src
└── main.rs
在本教程的其余部分,我们将处理src/main.rs文件,所以继续打开它。
像许多其他语言一样,Rust有一个将首先运行的主要函数。在println!是一个宏。正如你可能猜到的,这个程序是“你好世界!”的铁锈版本。
要构建和运行它,只需执行货物运行。
$ cargo run
Hello world!
如何阅读论点
我们的目标是让我们的CLI接受两个参数:第一个是动作,第二个是项目。
我们将从阅读用户输入的参数并将其打印出来开始。
将main的内容改为:
let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");
println!("{:?}, {:?}", action, item);
让我们从消化所有这些信息开始。
-
let[doc]将值绑定到变量。
-
std:: env:: args()[doc]是从标准库的env模块中引入的函数,它返回程序启动时的参数。由于它是一个迭代器,我们可以使用n th()函数访问存储在每个位置的值。位置0的参数是程序本身,这就是为什么我们从第一个元素开始阅读。
-
except()[doc]是为Option枚举定义的方法,它将返回值,或者如果不存在,将立即终止程序(Rust 术语中的恐慌),返回提供的消息。
因为程序可以在没有参数的情况下运行,Rust要求我们通过给我们一个Option类型来检查是否实际提供了一个值:值是否存在。
作为程序员,我们有责任确保在每种情况下采取适当的行动。
目前,如果没有提供参数,我们将立即退出程序。
让我们运行程序并传递两个参数。为此,将它们附加在--。例如:
$ cargo run -- hello world!
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/todo_cli hello 'world'\!''`
"hello", "world!"
如何使用自定义类型插入和保存数据
让我们思考一下我们的项目目标。我们希望阅读用户给出的参数,更新我们的待办事项列表,并将其存储起来以备使用。
为此,我们将实现我们自己的类型,在那里我们可以定义我们的方法来满足业务需求。
我们将使用Rust的结构,它让我们以一种干净的方式完成这两个任务。它避免了必须在main函数中编写所有代码。
如何定义我们的结构
由于我们将在以下步骤中大量键入HashMap,我们可以将其纳入范围,并为自己节省一些键入。
在我们文件的顶部添加这一行:
use std::collections::HashMap
这将让我们直接使用HashMap,而不需要每次都输入完整的路径。
在主函数下面,让我们添加以下代码:
struct Todo {
// use rust built in HashMap to store key - val pairs
map: HashMap<String, bool>,
}
这将定义我们的自定义To do类型:一个带有一个名为map的字段的结构。
这个字段是一个HashMap。您可以将其视为一种JavaScript对象,其中Rust要求我们声明键和值的类型。
- HashMap<String, bool>意味着我们有由Strings组成的键,还有一个布尔值:活动状态。
如何向我们的结构添加方法
方法就像常规函数——它们用fn关键字删除,它们接受参数,并且它们有返回值。
然而,它们不同于常规函数,因为它们是在结构的上下文中定义的,它们的第一个参数总是自定义的。
我们将在新添加的结构下面定义一个impl(实现)块。
impl Todo {
fn insert(&mut self, key: String) {
// insert a new item into our map.
// we pass true as value
self.map.insert(key, true);
}
}
这个函数非常简单:它只是对结构和键进行引用,并使用HashMap的内置插入方法将其安装到我们的映射中。
两条非常重要的信息:
-
mut[doc]使变量可变。
-
在Rust中,默认情况下每个变量都是不可变的。如果要更新值,需要使用mut关键字选择加入可变性。由于我们的函数通过添加新值来有效地更改映射,因此我们需要将其声明为可变的。
-
&[doc]表示引用*。*
-
您可以将var aible想象成指向存储值的内存位置的指针,而不是“值”本身。
-
在Rust术语中,这被称为借用,这意味着函数实际上并不拥有这个值,但它仅仅指向存储它的位置。
Rust所有权制度概述
有了前面关于借用和引用的提示,现在是简要谈论所有权的好时机。
所有权是Rust最独特的特征。它使Rust程序员能够编写程序,而不需要手动分配内存(如C/C++),同时仍然能够在没有垃圾收集器(如JavaScript或Python)的情况下运行,垃圾收集器会不断查看程序的内存以释放未使用的资源。
所有制有三条规则:
-
Rust中的每个值都有一个变量:它的所有者。
-
每个值一次只能有一个所有者。
-
当所有者超出范围时,该值将被删除。
Rust在编译时检查这些规则,这意味着如果希望在内存中释放值,您必须明确。
想想这个例子:
fn main() {
// the owner of the String is x
let x = String::from("Hello");
// we move the value inside this function.
// now doSomething is the owner of x.
// Rust will free the memory associated with x
// as soon as it goes out of "doSomething" scope.
doSomething(x);
// The compiler will throw an error since we tried to use the value x
// but since we moved it inside "doSomething"
// we cannot use it as we don't have ownership
// and the value may have been dropped.
println!("{}", x);
}
这个概念被广泛认为是学习Rust时最难掌握的,因为它对许多程序员来说可能是一个新概念。
您可以从Rust的官方文档中阅读关于所有权的更深入的解释。
对于所有制的来龙去脉,我们就不深究了。现在只要记住我上面提到的规则。试着思考,在每一步中,我们是否需要“拥有”这些值,然后删除它们,或者我们是否需要一个引用,这样它才能被保留。
例如,在上面的插入方法中,我们不想拥有map,因为我们仍然需要它将数据存储在某个地方。只有这样,我们才能最终释放分配的内存。
如何将地图保存到磁盘
由于这是一个演示应用程序,我们将采用最简单的长期存储解决方案:将地图写入磁盘文件。
让我们在impl块中创建一个新方法。
impl Todo {
// [rest of the code]
fn save(self) -> Result<(), std::io::Error> {
let mut content = String::new();
for (k, v) in self.map {
let record = format!("{}\t{}\n", k, v);
content.push_str(&record)
}
std::fs::write("db.txt", content)
}
}
-
->从函数中注释返回的类型。我们返回一个结果。
-
我们在地图上迭代,格式化每个字符串,用制表符分隔键和值,用新行分隔每行。
-
我们将格式化的字符串推送到内容变量中。
-
我们在一个名为db.txt.的文件中写入内容
重要的是要注意,储蓄是自我的所有权。
这是一个任意的决定,所以如果我们在调用保存后意外地试图更新地图,编译器会阻止我们(因为自己的内存会被释放)。
这是个人决定强制保存为最后使用的方法。这是一个完美的例子来展示如何使用Rust的内存管理来创建更严格的不能编译的代码(这有助于防止开发过程中的人为错误)。
如何在main中使用结构
现在我们有了这两种方法,我们可以使用它们。我们从阅读提供的参数开始就停止了main。现在,如果提供的操作是添加,我们将把该项目插入到文件中,并将其存储以备以后使用。
在两个参数绑定下面添加这些行:
fn main() {
// ...[arguments bindig code]
let mut todo = Todo {
map: HashMap::new(),
};
if action == "add" {
todo.insert(item);
match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
}
}
}
让我们看看我们在这里做什么:
-
let mut to do=To do让我们实例化一个结构,将其绑定为可变的。
-
我们使用.表示法调用TO DO插入方法。
-
我们匹配从保存函数返回的结果,并为这两种情况在屏幕上打印一条消息。
让我们测试一下。导航到您的终端并键入:
$ cargo run -- add "code rust"
todo saved
让我们检查保存的项目:
$ cat db.txt
code rust true
到目前为止,您可以在本要点中找到完整的代码片段。
如何从文件中读取
现在我们的程序有一个根本性的缺陷:每次我们“添加”时,我们都会覆盖地图,而不是更新它。这是因为我们每次运行程序时都会创建一个新的空映射。让我们解决这个问题。
在TO DO中添加一个新函数
我们将为我们的To do结构实现一个新函数。调用后,它将读取我们文件的内容,并返回使用先前存储的值填充的To do。请注意,这不是一个方法,因为它不把自己作为第一个参数。
我们将称之为new,这只是一个Rust约定(请参阅前面使用的HashMap:: new())。
让我们在impl块中添加以下代码:
impl Todo {
fn new() -> Result<Todo, std::io::Error> {
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("db.txt")?;
let mut content = String::new();
f.read_to_string(&mut content)?;
let map: HashMap<String, bool> = content
.lines()
.map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
.map(|v| (v[0], v[1]))
.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
.collect();
Ok(Todo { map })
}
// ...rest of the methods
}
不用担心,如果这感觉有点压倒性。我们使用了一种更加函数化的编程风格,主要是为了展示和介绍Rust支持其他语言中的许多范例,如迭代器、闭包和lambda函数。
让我们看看这里发生了什么:
-
我们正在定义一个新函数,它将返回一个To do结构或io: Error的Result。
-
我们通过定义各种Open Options来配置如何打开db.txt文件。最值得注意的是创建(true)标志,如果文件还不存在,它将创建该文件。
-
f.read_to_string(&mut含量)?读取文件中的所有字节,并将它们附加到内容字符串中。
-
注意:记得添加use std*:: io:*: Read;在文件的顶部,以及其他use语句,以使用read_to_string方法。
-
我们需要将文件的String类型转换为HashMap。我们通过将map变量绑定到这一行来实现: let map: HashMap<String, bool>。
-
这是编译器无法为我们定义类型的情况之一,所以我们自己声明它。
-
line[doc]为字符串的每一行创建一个迭代器,这意味着现在我们将迭代文件的每一个条目,因为我们在每个条目的末尾用/n格式化了它。
-
map[doc]接受一个闭包,并在迭代器的每个元素上调用它。
-
line.splitn(2,'\t')[doc]将在制表符上拆分我们的行。
-
在文档中描述的[doc]是标准库中最强大的方法之一:它将迭代器转换为相关的集合。
-
在这里,我们告诉map函数通过在方法中添加:: Vec<&str>来将我们的Split字符串转换为借用字符串切片的Vector。这告诉编译器在操作结束时我们想要哪个集合。
-
然后我们使用. map(|v|(v[0], v[1]))将其转换为一个元组。
-
然后我们使用. map(|(k, v)|(String:: from(k),bool::from_str(v).unwrap()))将元组的两个元素转换为String和布尔值。
-
注意:记得添加use std*::* str:: From Str;在文件的顶部以及其他use语句,以便能够使用from_str方法。
-
我们最终将它们收集到我们的HashMap中。这一次我们不需要声明类型,因为Rust从绑定声明中推断出它。
-
最后,如果我们从未遇到任何错误,我们将结构返回给调用方,并使用Ok(To do{map})。
-
请注意,与JavaScript非常相似,如果键和变量在结构中具有相同的名称,我们可以使用更短的表示法。
唷!
你做得很好!图像学分:rustacean.net/
替代办法
虽然map通常被认为更地道,但上面的也可以用for循环来实现。随意使用你最喜欢的。
fn new() -> Result<Todo, std::io::Error> {
// open the db file
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("db.txt")?;
// read its content into a new string
let mut content = String::new();
f.read_to_string(&mut content)?;
// allocate an empty HashMap
let mut map = HashMap::new();
// loop over each lines of the file
for entries in content.lines() {
// split and bind values
let mut values = entries.split('\t');
let key = values.next().expect("No Key");
let val = values.next().expect("No Value");
// insert them into HashMap
map.insert(String::from(key), bool::from_str(val).unwrap());
}
// Return Ok
Ok(Todo { map })
}
上面的代码在功能上等同于之前使用的更“功能性”的方法。
如何使用新功能
在main中,只需用以下方式更新我们的to do变量:
let mut todo = Todo::new().expect("Initialisation of db failed");
现在,如果我们回到终端并运行一堆“添加”命令,我们应该会看到我们的数据库正在正确更新:
$ cargo run -- add "make coffee"
todo saved
$ cargo run -- add "make pancakes"
todo saved
$ cat db.txt
make coffee true
make pancakes true
您可以在本要点中找到到目前为止编写的完整代码。
如何更新集合中的值
和所有TO DO应用一样,我们希望不仅能够添加项目,还能够切换项目并将其标记为已完成。
如何添加完整的方法
为此,让我们向我们的结构添加一个名为“完成”的新方法。在其中,我们获取对键的引用,并更新值,或者如果键不存在,则返回“无”。
impl Todo {
// [Rest of the TODO methods]
fn complete(&mut self, key: &String) -> Option<()> {
match self.map.get_mut(key) {
Some(v) => Some(*v = false),
None => None,
}
}
}
让我们看看这里发生了什么:
-
我们声明我们的函数返回类型:空选项。
-
整个方法返回Match表达式的结果,该结果将是空的some()或无。
-
self.map.get_mut[doc]将为我们提供对key值的可变引用,或者如果集合中不存在该值,则提供无。
-
我们使用*[doc]运算符取消引用该值并将其设置为false。
如何使用完整的方法
我们可以以类似于之前使用的插入的方式使用“完成”方法。
在main中,让我们检查作为参数传递的操作是否“完成”,方法是使用其他if语句:
// in the main function
if action == "add" {
// add action snippet
} else if action == "complete" {
match todo.complete(&item) {
None => println!("'{}' is not present in the list", item),
Some(_) => match todo.save() {
Ok(_) => println!("todo saved"),
Err(why) => println!("An error occurred: {}", why),
},
}
}
是时候分析我们在这里做什么了:
-
我们匹配todo. full(&project)方法返回的Option。
-
如果情况为无,我们会向用户打印警告,以获得更好的体验。
-
我们将项目作为引用与&项目传递给"to do. full"方法,以便该值仍然由该函数拥有。这意味着我们可以用它来印刷!宏在下面的行。
-
如果我们不这样做,这个值就会被“完成”所拥有,并下降到那里。
-
如果我们检测到一些值已返回,我们调用todo.save将更改永久存储到我们的文件中。
和前面一样,您可以在本要点中找到到目前为止编写的代码的快照。
尝试运行程序
是时候在我们的终端上试用我们在本地开发的应用了。让我们从删除db文件开始重新开始。
$ rm db.txt
然后添加和修改一些待办事项:
$ cargo run -- add "make coffee"
$ cargo run -- add "code rust"
$ cargo run -- complete "make coffee"
$ cat db.txt
make coffee false
code rust true
这意味着在这些命令的末尾,我们有一个完成的操作(煮咖啡)和一个待定的操作:code Rust。
假设我们想再煮一次咖啡:
$ cargo run -- add "make coffee
$ cat db.txt
make coffee true
code rust true
奖金:如何将其存储为带Serde的JSON
程序,即使是最小的,正在运行。但是让我们稍微扭转一下。来自JavaScript世界的我决定将我的值存储为JSON文件,而不是纯文本文件。
我们将借此机会看看如何安装和使用一个来自Rust开源社区的包,名为crates.io.
如何安装serde
要将新包安装到我们的项目中,请打开cargo.toml文件。在底部,您应该会看到一个[依赖]字段:只需将以下内容添加到文件中:
[dependencies]
serde_json = "1.0.60"
就这样。下一次,货物将编译我们的程序,还将下载并包含新包以及我们的代码。
如何更新To do:: New
我们要使用Serde的第一个地方是当我们读取db文件时。现在,我们希望读取JSON文件,而不是读取. txt。
在impl块中,让我们更新新函数:
// inside Todo impl block
fn new() -> Result<Todo, std::io::Error> {
// open db.json
let f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open("db.json")?;
// serialize json as HashMap
match serde_json::from_reader(f) {
Ok(map) => Ok(Todo { map }),
Err(e) if e.is_eof() => Ok(Todo {
map: HashMap::new(),
}),
Err(e) => panic!("An error occurred: {}", e),
}
}
显著的变化是:
-
文件选项不再有mut f绑定,因为我们不需要像以前那样手动将内容分配到字符串中。塞尔德会为我们处理的。
-
我们更新了我们的文件扩展名为db.json.
-
serde_json::from_reader[doc]将为我们反序列化该文件。它干扰映射的返回类型,并将尝试将我们的JSON转换为兼容的HashMap。如果一切顺利,我们会像以前一样返回To do结构。
-
Err(e)如果e.is_eof()是一个Match守卫,它允许我们细化Match语句的行为。
-
如果Serde返回一个错误一个过早的EOF(文件结束),这意味着该文件是完全空的(例如在第一次运行,或者如果我们删除了文件)。在这种情况下,我们从错误中恢复并返回一个空的HashMap。
-
对于所有其他错误,请立即退出程序。
如何更新Todo.save
我们想使用Serde的另一个地方是当我们将地图保存为JSON时。为此,更新impl块中的保存方法为:
// inside Todo impl block
fn save(self) -> Result<(), Box<dyn std::error::Error>> {
// open db.json
let f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.open("db.json")?;
// write to file with serde
serde_json::to_writer_pretty(f, &self.map)?;
Ok(())
}
和以前一样,让我们看看我们在这里改变了什么:
-
Box<dyn std:: error:: Error>.这次我们返回一个包含Rust泛型错误实现的Box。
-
简单地说,框是指向内存分配的指针。
-
因为我们可能会在打开文件时返回一个文件系统误差,或者在转换文件时返回一个Serde错误,所以我们真的不知道我们的函数可能会返回这两个错误中的哪一个。
-
因此,我们返回一个指向可能错误的指针,而不是错误本身,以便调用方处理它们。
-
我们当然已经更新了文件名以db.json匹配。
-
最后,我们让Serde完成繁重的工作,并将我们的HashMap写成JSON文件(打印精美)。
-
记得删除两个use std:: io:: Read;和use std:: str:: From Str;从文件的顶部,因为我们不再需要他们。
就这样。
现在,您可以运行程序并检查保存到文件中的输出。如果一切顺利,您现在应该看到您的待办事项保存为JSON。
您可以在本要点中找到这样编写的完整代码。
结束语、提示和其他资源
这是一段相当长的旅程,我很荣幸你能和我一起走。
我希望你学到了一些东西,并通过这次介绍激发了你的好奇心。别忘了,我们使用的是一种非常“低级”的语言,然而审查代码可能对大多数人来说都很熟悉。
这就是Rust吸引我的地方——事实上,它让我能够编写速度极快、内存高效的代码,而不用担心随之而来的责任感:我知道编译器会在我身边,在我的代码运行之前就停止它。
在结束之前,我想与您分享一些额外的技巧和资源,以帮助您在Rust之旅中前进:
-
Rust fmt是一个非常方便的工具,您可以运行它来按照一致的模式格式化您的代码。不再浪费时间配置您最喜爱的linter插件。
-
货物检查[doc]将尝试在不运行的情况下编译代码:这在开发时是一个非常有用的命令,在开发时,您只想检查代码的正确性,而不实际运行它。
-
Rust附带了一个集成的测试套件和一个生成文档的工具:货物测试和货物文档。这次我们没有提到它们,因为教程看起来相当密集。也许在未来。
要了解更多关于这门语言的信息,我认为最好的资源是:
-
Rust官方网站,收集所有信息。
-
如果您喜欢通过聊天进行互动,Rust's Disode服务器有一个非常活跃和有用的社区。
-
如果你喜欢通过阅读来学习,《锈编程语言》这本书是你的正确选择。
-
如果你更喜欢视频类型,瑞安·莱维克对铁锈视频系列的介绍是一个惊人的资源。
您可以在GitHub上找到本文的源代码。
封面图片来自rustacean.net/.
感谢阅读和快乐编码!