穿越火线怎么执行程序(穿越火线启动程序在哪)

访客2023-10-07 21:16:4626

原文链接:www.freecodecamp.org/news/how-to…

作者:

Claudio Restifo

文章日期:2021.1.4

文章首发于知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

$ cargo new todo-cli $ tree . . ├── Cargo.toml └── src └── main.rs 复制代码

就像很多其他的软件,Rust 也有一个 main 函数,运行程序时,main 函数是入口。

下面我们来看,目前自动生成的 main 函数

fn main() { println!("Hello, world!"); } 复制代码

fn 相当于 js 里的 function。 println! 不是函数,而是宏。这个程序就是 rust 版本的 “hello world”

执行这个程序的命令是 cargo run

$ cargo run Hello, world! 复制代码

fn 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 看起来像 js 的 let,实际更像 js 的 const,因为 let 定义了一个不变量。

std::env::args() 是标准库的函数,提供了处理命令行输入的能力。args() 是一个 iterator,在 rust 里 iterator 可以通过 nth() 来获得第几个变量的值。 位置 0 是 程序本身,第一个变量从 1 开始。

expect() 是枚举 Option 的方法,如果 Option 不存在,则终止当前程序,并且打印 expect 里的内容。

$ cargo run thread 'main' panicked at 'Please specify an action', crates/todo-cli/src/main.rs:2:42 note: run with `RUST_BACKTRACE=1` environment variable to display a BACKtrace $ cargo run aa thread 'main' panicked at 'Please specify an item', crates/todo-cli/src/main.rs:3:40 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace $ cargo run aa bb "aa", "bb" 复制代码

注意,如果测试的参数没有 - 等命令行常用的符号,可以直接用上面的命令来进行调试。但是,一般 cli 的常用选项都会加 - ,这时 - 与 cargo 自身的命令会起冲突,所以要加 — 进行开发调试。下面这个例子是一个调试命令行工具更常用的写法:

$ cargo run -- aa bb "aa", "bb" 复制代码

下面我们把输入的内容存到一个数据结构里,在 Rust 里,使用 struct 来定义数据结构。类似与 js 的 object。

use std::collections::HashMap; struct Todo { // 使用 rust 内置的 HashMap 来存储 Key Value 对 map: HashMap<String, bool>, } 复制代码

现在就有了自定义的 Todo 类型:一个 struct,有一个字段,这个字段的名字叫 map,类型是 HashMap<String, bool>。这个 HashMap 的 key 是 String 类型的,value 是 boolean 类型的。

现在我们来向struct Todo 增加方法,Rust 增加方法的写法和 Golang 有相似的地方。

impl Todo { fn insert(&mut self, key: String) { // 往 map 里插入新数据 // 把 true 作为值 self.map.insert(key, true); } } 复制代码

impl 是 implementaion 的简写,相当于给 Todo 实现方法的地方。对于给 Todo 增加的每一个方法和定义普通函数类似,但是第一个参数,总是 self。

上述的方法,给 map 增加了一个 key-value 对。insert 是 map 的内置方法。

出现了新的关键字:

  • mut:这个关键字让变量编程了可变变量。在 Rust 中,所有的变量都是默认不可变的。如果你想要更新一个变量的值,你需要加上 mut 关键字。因为 insert 方法要改变 map 的值,相当于改了 self 的值,所以要在声明 self 时,加上 mut 关键字。
  • & :表明这是一个引用。可以想象 self 是一个内存地址的指针,而不是值本身。

在 rust 中,如果你获得了一个 &,表示你 borrow 了这个变量,这表明,这个函数并不拥有 self 的值,而是借用了 self 的值。

Rust 所有权系统简介

有了上面关于 borrow 和 reference 的代码,现在可以聊聊所有权了。

所有权是 Rust 最独特的功能。这个功能让 Rust 可以不用手动处理内存,同时还不需要 GC。

所有权系统有三条规则:

  • 每一个 Rust 的变量都有自己的所有者
  • 每一个变量在一个时刻只能有一个所有者
  • 当所有者离开作用域,变量就会被丢弃

Rust 会在编译时对这些规则进行检查,这代表着,开发者必须显示标注你用的值什么时候要被释放。

fn main() { // String 的所有者是 x let x = String::from("Hello"); // 我们把 x 移入到了函数中 // 现在 doSomething 是 x 的所有者 // 当离开 doSomething 时,x 的内存就会被释放 doSomething(x); // 编译器会抛出异常 // 因为我们把 x 的所有权交给 doSomething 以后,我们已经没有 x 了。 // x 可能也被 drop 了 println!("{}", x); } 复制代码

这个概念被认为是学习 Rust 最难的一个事情之一,因为这个概念,在其他语言里没有。

你可以在官方文档里读到更多关于所有权的信息。

在这个简单的程序里,不会涉及太多关于所有权的问题。在每一步,如果需要获得一个变量的所有权,并且释放它,或者需要一个变量的引用,代表着还要保留变量。

在这个 insert 的例子里,我们不想去拥有 map,我们还需要它在某个地方保留这些数据。只有最后我们才能清空内存。

如何把 map 存入到磁盘上

因为这是一个示例程序,所以我们使用最简单的方案,把 map 存入到磁盘上的一个文件里。

impl Todo { // 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) } } 复制代码

  • → 代表函数的返回值。这里返回了一个 Result
  • 在这个方法里,我们遍历了map 的所有值,把 key 和 value 用 tab 进行分隔
  • 然后把所有的内容装入 content 变量中
  • 最后把 content 的内容写入到 db.txt 文件中

这里要注意,save 函数获得了 self 的所有权。这是故意这么做的,这样如果我们执行了 save,之后就不能在去更新 map 了。

这么设计,save 函数只能在最后执行,否则就会报错。也是一个使用 rust 的特性进行内存管理策略的例子。

如何在 main 里使用 struct

现在我们在 main 函数里实例化写好的 Todo 结构体。

// ... 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 todo= Todo 这行代码实例化了一个结构体,并且把这个变量声明为可变变量
  • 调用结构体的方法使用 . 符号
  • 对于 save 的返回的 Result 的结果,我们使用了 Rust 的模式匹配机制,把成功和失败的两种情况进行了处理。

$ cargo run -- add "code rust" "add", "code rust" todo saved $ cat db.txt code rust true 复制代码如何从一个文件读数据

目前的程序有一个问题,每一次增加,都是把之前的内容进行了替换,而不是更新。因为每一次我们的 map 都是一个新 map。

在 TODO 里增加一个新函数

我们创建一个新的函数来把之前写入到 db.txt 里的内容读出来。

我们把这个函数称之为 new,new 有点像 js 里的 constructor,但是 new 的名字可以是任意的。

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, v)) .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap())) .collect(); Ok(Todo { map }) } // ...其余的方法 } 复制代码

  • new 函数,的返回是一个 Result,如果成功则返回 Todo,如果失败,则返回 std::io::Error
  • 打开 db.txt 时,使用了 OpenOptions。打开的这个文件,可读,可写,create(true) 说明,如果这个文件不存在,则创建。
  • ? 是 rust 对于 Result 展开的语法糖,如果遇到 Error,则会立即抛出 Error,如果一切顺利,则获得 Result 中的类型。
  • f.read_to_string(&mut content)?读取了文件所有的内容,并且把文件的内容放入了 content 中。这里需要增加 use std::io::Read;否则 read_to_string 会报错。
  • 读取的内容是一个文本,我们需要把文本转换成一个 HashMap。let map: HashMap<String, bool> 声明做了这个 HashMap。这里编译器并不能帮我们推断类型,所以需要显式声明这个 map 的类型。
  • lines() 对一个字符串的每一行创建了一个迭代器。
  • map 会调用一个闭包,然后作用闭包到迭代器的每一个元素中。
  • line.splitn(2,’t’) 会把每一行字符串按照 tab 进行分隔成 2 个。
  • collect::<Vec<&str>>() 是标准库非常强大的一个方法,这个方法把一个迭代器转化为一个集合类型。这里把分隔好的字符串,转化成了Vec<&str>。
  • .map(|v| (v, v)) 继续转化内容为一对 tuple
  • 然后.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap())) 把这个 tuple 转化为 String 和 boolean。注意这里要增加 use std::str::FromStr;
  • 最后调用 collect(),获得最终的 HashMap。因为声明的 map 有类型,所以 collect 不再需要类型。
  • 如果没有遇到任何错误,最后返回 *Ok*(Todo { map }),和 JavaScript 类似,如果 struct 的元素的名字和变量名字一样,可以简写。
另一个实现方式

使用 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 }) } 复制代码

上面这个实现与更“函数式”的实现结果是等价的。

如何使用新的函数

现在需要更新初始化 Todo 的代码

let mut todo = Todo::new().expect("Initialisation of db failed"); 复制代码

现在每次运行的结果,都会保存到 db.txt 中

$ cargo run -- add "from js to rust" todo saved $ cargo run -- add "from js to rust 2" todo saved $ cat db.txt from js to rust 2 true from js to rust true 复制代码如何更新集合中的数据

就像大多数 TODO 应用,不仅要增加条目,在完成时,还要标识完成。

增加 complete 方法

impl Todo { // fn complete(&mut self, key: &String) -> Option<()> { match self.map.get_mut(key) { Some(v) => Some(*v = false), None => None, } } } 复制代码

  • complete 方法的返回值是一个空的 Option
  • 方法体根据匹配结果要么是一个空的 Some,要么是一个 None
  • self.map.get_mut 会给我们一个 key 的可变引用,如果没有找到这个 key,则返回 None
  • 把变量进行去引用,然后把值改为 false
如何使用 complete 方法

我们可以扩展之前 insert 在的代码。

// 在 main 函数中 if action == "add" { // 增加 complete 方法 } 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.complete(&item) 的返回结果进行匹配
  • 如果为 None,则提示友好的信息告诉用户没有这个行为。我们给 complete 传入的是&item,所以所有权,仍然在当前代码。所以我们可以在 println! 中使用 item。如果不这么做,item 的值会被 complete 获得,接下来就不能用了。
  • 如果我们检测到 Some,说明对数据进行了更改,这时调用 save 方法,保存当前的内容。
运行代码

$ rm db.txt $ cargo run -- add "make tea" $ cargo run -- add "code rust" $ cargo run -- complete "make tea" $ cat db.txt make tea false code rust true 复制代码赠品:如何用 JSON 进行存储

这个程序,虽然小巧,但是可以运行。因为我们来自 JavaScript 的世界,所以我们把最后的输出改为 JSON。

这里需要使用第三方的库,所以我们去 Rust 寻找第三方库的网站 crates.io。

如何安装 serde

按照第三方库在项目中,打开 cargo.toml,在

serde_json = "1.0.60" 复制代码

保存以后,在编译时,cargo 会去下载 serde 的 crate

更新代码

首先更新 new 方法,这里不再打开一个 txt 文件,而是 json 文件

// 在 Todo impl 代码块中 fn new() -> Result<Todo, std::io::Error> { // 打开 db.json let f = std::fs::OpenOptions::new() .write(true) .create(true) .read(true) .open("db.json")?; // 序列化 json 为 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,因为我们不再手动处理内容为 String。Serde 都会帮我们做这些事。
  • 文件扩展名改为 json
  • serde_json::from_reader 会把文件反序列化给我们。并且会进行自动转化,如果一切顺利,则获得和之前一样的 Todo
  • Err(e) if e.is_eof() 是一个 Match guard,可以定一个一个 Match 语句的行为。如果Serde 返回的错误是 EOF ,这说明这个文件是空文件。如果是一个空文件,则新建一个空 HashMap。
  • 所有其他的错误,则直接 panic
如何更新 save

修改 save 代码

// 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,这里返回了一个 Box 包含 Rust 的泛型错误。box 是一个指向内存的指针。因为,这里即可能是一个文件系统的错误,也可能是 serde 的错误,所以我们并不知道返回的错误是什么。所以使用指针来保存错误,而不是返回错误本身。
  • 把存储的文件内容改为 db.json
  • 最后,serde 帮我们把文件内容存储为 JSON
  • 这时就不再需要use std::io::Read; 和 use std::str::FromStr;

现在再重新运行你的程序,存储的格式就变为了 JSON。

控制面板

您好,欢迎到访网站!
  查看权限

最新留言