所有权以及优化
那么上一次我们已经创建了一个数据库的 structs
,并且成功将内容解析并写入数据库示例。
本文仅作于我的学习笔记,不保证完全正确。
1 | use std::collections::HashMap; |
但是你可以看到,每次调用的时候都会覆盖上一次的内容,而不是累积下去,也对子命令没有处理。
那么我们现在来构造一个 insert()
的 impl
方法把数据添加到数据库。
最终看起来就像是 database.insert(key, value)
写入内存HashMap数据库
1 | fn insert(key: String, value: String) {} |
那么理所当然的应该是这样的吧?错了,这是对于 struct
的操作,而不是对于特定实例的操作。
方法的开头第一个参数应该是 self
,指的是 Database
类的实例。
1 | fn insert(self, key: String, value: String) {} |
当然你也可以用 Database::insert(database, key, value)
,不过那样也是同样的效果,并且更长而已。
1 | fn insert(self, key: String, value: String) { |
然后就是把数据插入到数据库的 map
里面了。
但这个时候Rust
又报错了,因为这里改变了self
,因此self
需要是可变化的,之前说过 Rust
需要特别以 mut
表明参数是可变化的。
1 | fn insert(mut self, key: String, value: String) { |
那么这时候如果我们想要同时让全大写的 key
同时储存,我们只需要多加一个 database.insert(key.to_uppercase(), value)
就行了?
1 | database.insert(key, 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
.
这是因为定义这个 self
为 self
,因为我们在调用 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
的方法。
1 | fn insert(&self, key: String, value: String) { |
这时候你就可以看到第一个错误不见了,因为我们只是借了 Database
,而并没有转移所有权。
但是现在又出现了一个新的问题,
cannot borrow self.map
as mutable, as it is behind a &
reference
它说我们需要用 &mut self
才能获得一个可变化的 borrow
。
1 | fn insert(&mut self, key: String, value: String) { |
改了之后你会发现,又多了两个错误,这次是在 main
里面的 database
cannot borrow database
as mutable, as it is not declared as mutable
那么为什么之前用 mut self
的时候没有需要上面也 mut
呢,因为那时候改变的是 self
而不是 database
。
而现在因为只是借了所有权,所以实质上改变的是 database
而不是 self
,因此只有需要改变的参数才需要是 mut
。
1 | database.insert(key, value); |
那么现在就剩一个错误,value
被移动到了 insert()
里面,但是 value
事实上现在没有被 drop
,因为现在 database
只会在 main()
结束的时候 drop
。
我们解决的方法是使用一个 clone()
, 这个函数以 borrow
的字符串做参数并返回了一下新的有所有权的字符串,因此返回的字符串肯定不是原来的字符串。
1 | database.insert(key.to_uppercase(), value.clone()); |
写入文件
现在我们已经把数据获取并写入本地数据库,但是你会发现数据库文件还是没有写入的。
那我们创建一个 flush()
方法来把数据写入数据库。
作为一个方法,并且返回这个写入是否成功的 Result
。
1 | fn flush(self) -> std::io::Result<()> { |
这里就是用一个 for loop
把 HashMap
的内容通过 String::push_str()
加到 contents
里面。
如果你看一下 push_str()
的输入类型会发现是 &str
, 那么为什么能传入 &String
呢。
那是因为自动 deref
, 类型可以被转型。虽然听上去很复杂,其实没有很复杂,但确实超出了这次的范围。
虽然说这里其实不需要转移所有权到 flush()
里面,只需要借 Database
就可以了。
改了 &self
就需要把后面 self.map
改成 &self.map
,因为self
没有所有权,因此也没办法转移其中的所有权。
但在 API 设计的角度,我们可以使用所有权来让用户不要在 flush()
了之后再向数据库推送资讯。
优化
在这个 for loop
里面,我们每一次循环都在创造,借用然后马上销毁这个 pair
,这看上去不是很有效率。
我们可以直接传入,就不需要一个中转参数。
1 | for (key, value) in & self .map { |
这段代码虽然比较长,但是事实上更有效率。
Drop
在编程的时候很容易忘记你要 flush()
数据库,那么有没有什么方法能够自动来调用呢?
我们可以使用 Drop
, Drop
顾名思义会在被销毁的时候调用。
1 | struct Database { |
要使用 Drop
我们需要使用 impl Drop for Database
,然后在里面实现 drop
的功能,这里传入的一定是一个 &mut self
,因此我们不能够调用 self.flush()
,而必须把函数内容搬出去,或者有两段重复代码。
这是为了如果用户想要处理数据写入的错误,可以自行调用 flush()
,然而现在调用了之后最终也会 Drop
,也就是会运行两次。
所以我们可以在 Database
加一个字段 flushed: bool
,创建为 false
,flush
之后转为 true
,如果 true
就不 flush
。
1 | use std::collections::HashMap; |