所有权以及优化

那么上一次我们已经创建了一个数据库的 structs ,并且成功将内容解析并写入数据库示例。

本文仅作于我的学习笔记,不保证完全正确。

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use std::collections::HashMap;
use std::io::Error;

fn main() {
let mut args = std::env::args().skip(1);
let key = args.next().unwrap();
let value = args.next().unwrap();
println!("The key is {}, the value is {}", key, value);
let contents = std::format!("{}\t{}\n", key, value);
std::fs::write("kv.db", contents).unwrap();
let database = Database::new().expect("Database::new() crashed");
}

struct Database {
map: HashMap<String, String>,
}

impl Database {
fn new() -> Result<Database, Error> {
let result = std::fs::read_to_string("kv.db")?;
let mut map = HashMap::new();
for line in result.lines() {
let (key, value) = line.split_once('\t').expect("Corrupted Data");
map.insert(key.to_owned(), value.to_owned());
}
Ok(
Database {
map,
}
)
}
}

但是你可以看到,每次调用的时候都会覆盖上一次的内容,而不是累积下去,也对子命令没有处理。
那么我们现在来构造一个 insert()impl 方法把数据添加到数据库。
最终看起来就像是 database.insert(key, value)

写入内存HashMap数据库

main.rs
1
fn insert(key: String, value: String) {}

那么理所当然的应该是这样的吧?错了,这是对于 struct 的操作,而不是对于特定实例的操作。
方法的开头第一个参数应该是 self ,指的是 Database 类的实例。

main.rs
1
fn insert(self, key: String, value: String) {}

当然你也可以用 Database::insert(database, key, value) ,不过那样也是同样的效果,并且更长而已。

main.rs
1
2
3
fn insert(self, key: String, value: String) {
self.map.insert(key, value);
}

然后就是把数据插入到数据库的 map 里面了。
但这个时候Rust又报错了,因为这里改变了self,因此self需要是可变化的,之前说过 Rust 需要特别以 mut 表明参数是可变化的。

main.rs
1
2
3
fn insert(mut self, key: String, value: String) {
self.map.insert(key, value);
}

那么这时候如果我们想要同时让全大写的 key 同时储存,我们只需要多加一个 database.insert(key.to_uppercase(), value) 就行了?

main.rs
1
2
database.insert(key, value);
database.insert(key.to_uppercase(), value);

错了,这时候你的 IDE 又开始报错了。

move occurs because database has type Database, which does not implement the Copy trait.
database moved due to this method call.
this function takes ownership of the receiver self, which moves database.

这是因为定义这个 selfself,因为我们在调用 insert() 时,Database 的所有权被转移到了 self ,并且调用完了,self
跑出作用域了,然后就被 drop 了,这也包括 HashMap 以及它的内容。
在这个错误里面,就是你的所有权已经给了 self,但你还在使用 Database

如果你尝试使用这个已经被drop的实例,这在C++中被称为Use After Free,但在Rust里面,这是不可能做到的。

同时还有一个错误:

borrow of moved value: key

看上去和上面的错误很相似。这是因为当我们使用 String 的时候,就代表该字符串需要被移动到函数内。
当我们移动到了函数内,我们就不能再用 key 了。
为什么是 borrow of moved value 而不是 use of moved value ,这是因为 to_uppercase() 的输入为 &self,它 borrow
了这个所有权,也就是一个不传递所有权的方法。
事实上,如果你把两行互换,你就会发现这个关于 key 的错误没了,那是因为 key 的所有权并没有被转移,并且返回的字符串可以确定是新的字符串。

那我们要怎么解决这些问题,我们可以使用以上 borrow 的方法。

main.rs
1
2
3
fn insert(&self, key: String, value: String) {
self.map.insert(key, value);
}

这时候你就可以看到第一个错误不见了,因为我们只是借了 Database ,而并没有转移所有权。
但是现在又出现了一个新的问题,

cannot borrow self.map as mutable, as it is behind a & reference

它说我们需要用 &mut self 才能获得一个可变化的 borrow

main.rs
1
2
3
fn insert(&mut self, key: String, value: String) {
self.map.insert(key, value);
}

改了之后你会发现,又多了两个错误,这次是在 main 里面的 database

cannot borrow database as mutable, as it is not declared as mutable

那么为什么之前用 mut self 的时候没有需要上面也 mut 呢,因为那时候改变的是 self 而不是 database
而现在因为只是借了所有权,所以实质上改变的是 database 而不是 self ,因此只有需要改变的参数才需要是 mut

main.rs
1
2
database.insert(key, value);
database.insert(key.to_uppercase(), value);

那么现在就剩一个错误,value 被移动到了 insert() 里面,但是 value 事实上现在没有被 drop ,因为现在 database
只会在 main() 结束的时候 drop
我们解决的方法是使用一个 clone() , 这个函数以 borrow 的字符串做参数并返回了一下新的有所有权的字符串,因此返回的字符串肯定不是原来的字符串。

main.rs
1
2
database.insert(key.to_uppercase(), value.clone());
database.insert(key, value);

写入文件

现在我们已经把数据获取并写入本地数据库,但是你会发现数据库文件还是没有写入的。
那我们创建一个 flush() 方法来把数据写入数据库。
作为一个方法,并且返回这个写入是否成功的 Result

main.rs
1
2
3
4
5
6
7
8
fn flush(self) -> std::io::Result<()> {
let mut contents = String::new();
for (key, value) in self.map {
let pair = format!("{}\t{}\n", key, value);
contents.push_str(&pair);
}
std::fs::write("kv.db", contents)
}

这里就是用一个 for loopHashMap 的内容通过 String::push_str() 加到 contents 里面。

如果你看一下 push_str() 的输入类型会发现是 &str , 那么为什么能传入 &String 呢。
那是因为自动 deref , 类型可以被转型。虽然听上去很复杂,其实没有很复杂,但确实超出了这次的范围。

虽然说这里其实不需要转移所有权到 flush() 里面,只需要借 Database 就可以了。
改了 &self 就需要把后面 self.map 改成 &self.map,因为self没有所有权,因此也没办法转移其中的所有权。
但在 API 设计的角度,我们可以使用所有权来让用户不要在 flush() 了之后再向数据库推送资讯。

优化

在这个 for loop 里面,我们每一次循环都在创造,借用然后马上销毁这个 pair ,这看上去不是很有效率。
我们可以直接传入,就不需要一个中转参数。

main.rs
1
2
3
4
5
6
for (key, value) in & self .map {
contents.push_str(key);
contents.push('\t');
contents.push_str(value);
contents.push('\n');
}

这段代码虽然比较长,但是事实上更有效率。

Drop

在编程的时候很容易忘记你要 flush() 数据库,那么有没有什么方法能够自动来调用呢?
我们可以使用 DropDrop 顾名思义会在被销毁的时候调用。

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct Database {
map: HashMap<String, String>,
}

impl Database {
fn new() -> Result<Database, Error> { ... }
fn insert(&mut self, key: String, value: String) { ... }
fn flush(self) -> std::io::Result<()> {
flush(&self)
}
}

impl Drop for Database {
fn drop(&mut self) {
flush(self).unwrap();
}
}

fn flush(database: &Database) -> std::io::Result<()> {
let mut contents = String::new();
for (key, value) in &database.map {
contents.push_str(key);
contents.push('\t');
contents.push_str(value);
contents.push('\n');
}
std::fs::write("kv.db", contents)
}

要使用 Drop 我们需要使用 impl Drop for Database ,然后在里面实现 drop 的功能,这里传入的一定是一个 &mut self
,因此我们不能够调用 self.flush(),而必须把函数内容搬出去,或者有两段重复代码。
这是为了如果用户想要处理数据写入的错误,可以自行调用 flush() ,然而现在调用了之后最终也会 Drop ,也就是会运行两次。
所以我们可以在 Database 加一个字段 flushed: bool ,创建为 falseflush 之后转为 true,如果 true 就不 flush

main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
use std::collections::HashMap;
use std::io::Error;

fn main() {
let mut args = std::env::args().skip(1);
let key = args.next().unwrap();
let value = args.next().unwrap();
println!("The key is {}, the value is {}", key, value);

let mut database = Database::new().expect("Database::new() crashed");
database.insert(key.to_uppercase(), value.clone());
database.insert(key, value);
}

struct Database {
map: HashMap<String, String>,
flush: bool,
}

impl Database {
fn new() -> Result<Database, Error> {
let result = std::fs::read_to_string("kv.db")?;
let mut map = HashMap::new();
for line in result.lines() {
let (key, value) = line.split_once('\t').expect("Corrupted Data");
map.insert(key.to_owned(), value.to_owned());
}
Ok(
Database {
map,
flush: false
}
)
}
fn insert(&mut self, key: String, value: String) {
self.map.insert(key, value);
}
fn flush(mut self) -> std::io::Result<()> {
self.flush = true;
flush(&self)
}
}

impl Drop for Database {
fn drop(&mut self) {
if !self.flush {
flush(self).unwrap();
}
}
}

fn flush(database: &Database) -> std::io::Result<()> {
let mut contents = String::new();
for (key, value) in &database.map {
contents.push_str(key);
contents.push('\t');
contents.push_str(value);
contents.push('\n');
}
std::fs::write("kv.db", contents)
}