原文链接:
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 的内置方法。
出现了新的关键字:
在 rust 中,如果你获得了一个 &,表示你 borrow 了这个变量,这表明,这个函数并不拥有 self 的值,而是借用了 self 的值。
Rust 所有权系统简介
有了上面关于 borrow 和 reference 的代码,现在可以聊聊所有权了。
所有权是 Rust 最独特的功能。这个功能让 Rust 可以不用手动处理内存(就好像 C 和 C++),同时还不需要 GC(就好像 JavaScript 和 Python)。
所有权系统有三条规则:
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) }}复制代码
这里要注意,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), } }复制代码
$ 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 }) }// ...其余的方法}复制代码
另一个实现方式
使用 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 方法
我们可以扩展之前 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), }, }}复制代码
运行代码
$ 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), }}复制代码
如何更新 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(())}复制代码
现在再重新运行你的程序,存储的格式就变为了 JSON。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!