[from js to rust 系列][简单应用-01]使用 Rust 写一个 To-Do [译文]

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

文章日期: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 runthread 'main' panicked at 'Please specify an action', crates/todo-cli/src/main.rs:2:42note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace$ cargo run aathread 'main' panicked at 'Please specify an item', crates/todo-cli/src/main.rs:3:40note: 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 可以不用手动处理内存(就好像 C 和 C++),同时还不需要 GC(就好像 JavaScript 和 Python)。

    所有权系统有三条规则:

  • 每一个 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.txtcode 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[0], v[1]))            .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[0], v[1])) 继续转化内容为一对 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.txtfrom js to rust 2       truefrom js to rust true复制代码

    如何更新集合中的数据

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

    增加 complete 方法

    impl Todo {// [其余的 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.txtmake tea        falsecode rust       true复制代码

    赠品:如何用 JSON 进行存储

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

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

    如何安装 serde

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

    [dependencies]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 (end of file),这说明这个文件是空文件(例如第一次运行,或者我们删除了文件)。如果是一个空文件,则新建一个空 HashMap。
  • 所有其他的错误,则直接 panic
  • 如何更新 save

    修改 save 代码

    // inside Todo impl blockfn 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(pretty printed 格式)
  • 这时就不再需要use std::io::Read; 和 use std::str::FromStr;
  • 现在再重新运行你的程序,存储的格式就变为了 JSON。

    声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

    上一篇 2022年5月1日
    下一篇 2022年5月1日

    相关推荐