我正在尝试使用HashMap
创建内存数据库。我有一个结构Person
:
struct Person {
id: i64,
name: String,
}
impl Person {
pub fn new(id: i64, name: &str) -> Person {
Person {
id: id,
name: name.to_string(),
}
}
pub fn set_name(&mut self, name: &str) {
self.name = name.to_string();
}
}
我有结构Database
:
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
struct Database {
db: Arc<Mutex<HashMap<i64, Person>>>,
}
impl Database {
pub fn new() -> Database {
Database {
db: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn add_person(&mut self, id: i64, person: Person) {
self.db.lock().unwrap().insert(id, person);
}
pub fn get_person(&self, id: i64) -> Option<&mut Person> {
self.db.lock().unwrap().get_mut(&id)
}
}
使用此数据库的代码:
let mut db = Database::new();
db.add_person(1, Person::new(1, "Bob"));
我想更改person
的名称:
let mut person = db.get_person(1).unwrap();
person.set_name("Bill");
complete code in the Rust playground。
编译时,我遇到了Rust生命周期的问题:
error[E0597]: borrowed value does not live long enough
--> src/main.rs:39:9
|
39 | self.db.lock().unwrap().get_mut(&id)
| ^^^^^^^^^^^^^^^^^^^^^^^ temporary value does not live long enough
40 | }
| - temporary value only lives until here
|
note: borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 38:5...
--> src/main.rs:38:5
|
38 | / pub fn get_person(&self, id: i64) -> Option<&mut Person> {
39 | | self.db.lock().unwrap().get_mut(&id)
40 | | }
| |_____^
如何实施这种方法?
答案 0 :(得分:11)
编译器拒绝您的代码,因为它违反了Rust强制执行的正确性模型,可能导致崩溃。例如,如果允许get_person()
编译,可以从两个线程调用它并在没有互斥锁保护的情况下修改底层对象,从而导致内部String
对象上的数据争用。更糟糕的是,即使在单线程场景中,也可能通过执行以下操作来肆虐:
let mut ref1 = db.get_person(1).unwrap();
let mut ref2 = db.get_person(1).unwrap();
// ERROR - two mutable references to the same object!
let vec: Vec<Person> = vec![];
vec.push(*ref1); // move referenced object to the vector
println!(*ref2); // CRASH - object already moved
要更正代码,您需要调整设计以满足以下约束条件:
add_person
方法已经符合这两个规则,因为它会占用您传递它的对象,并将其移动到数据库中。
如果我们修改get_person()
以返回不可变引用怎么办?
pub fn get_person(&self, id: i64) -> Option<&Person> {
self.db.lock().unwrap().get(&id)
}
即使这个看似无辜的版本仍然无法编译!那是因为它违反了第一条规则。 Rust无法静态证明引用不会比数据库本身更长,因为数据库是在堆上分配并引用计数的,因此可以随时删除它。但即使有可能以某种方式明确地声明引用的生命周期可证明不会比数据库更长,但在解锁互斥锁之后保留引用将允许数据争用。没有办法实现get_person()
并仍保留线程安全性。
读取的线程安全实现可以选择返回数据的副本。 Person
可以实现clone()
方法,get_person()
可以像这样调用它:
#[derive(Clone)]
struct Person {
id: i64,
name: String
}
// ...
pub fn get_person(&self, id: i64) -> Option<Person> {
self.db.lock().unwrap().get(&id).cloned()
}
此类更改不适用于get_person()
的其他用例,其中该方法用于获取可变引用以更改数据库中的人员的明确目的。获取对共享资源的可变引用违反了第二条规则,并可能导致崩溃,如上所示。有几种方法可以使它安全。一种是通过在数据库中提供代理来设置每个Person
字段:
pub fn set_person_name(&self, id: i64, new_name: String) -> bool {
match self.db.lock().unwrap().get_mut(&id) {
Some(mut person) => {
person.name = new_name;
true
}
None => false
}
}
随着Person
上的字段数量的增加,这将很快变得乏味。它也可能变慢,因为每次访问都必须获取一个单独的互斥锁。
幸运的是,有一种更好的方法来实现对条目的修改。请记住,使用可变引用违反了规则,除非 Rust可以证明引用不会“逃避”使用它的块。这可以通过反转控件来确保 - 而不是返回可变引用的get_person()
,我们可以引入modify_person()
将可变引用传递给可调用的引用1>随心所欲地做任何事。例如:
pub fn modify_person<F>(&self, id: i64, f: F) where F: FnOnce(Option<&mut Person>) {
f(self.db.lock().unwrap().get_mut(&id))
}
用法如下:
fn main() {
let mut db = Database::new();
db.add_person(1, Person::new(1, "Bob"));
assert!(db.get_person(1).unwrap().name == "Bob");
db.modify_person(1, |person| {
person.unwrap().set_name("Bill");
});
}
最后,如果您担心get_person()
克隆Person
的性能仅仅是为了检查它,那么创建一个modify_person
的不可变版本是很容易的get_person()
的非复制替代方法:
pub fn read_person<F, R>(&self, id: i64, f: F) -> R
where F: FnOnce(Option<&Person>) -> R {
f(self.db.lock().unwrap().get(&id))
}
除了对Person
进行共享引用之外,read_person
还允许闭包在它选择时返回一个值,通常是它从它接收的对象中获取的值。它的用法类似于modify_person
的用法,增加了返回值的可能性:
// if Person had an "age" field, we could obtain it like this:
let person_age = db.read_person(1, |person| person.unwrap().age);
// equivalent to the copying definition of db.get_person():
let person_copy = db.read_person(1, |person| person.cloned());
答案 1 :(得分:2)
这篇文章使用引用的模式&#34;控制反转&#34;在well explained answer中只添加 only 糖来演示内存数据库的另一个api。
使用宏规则可以公开db client api,如下所示:
fn main() {
let db = Database::new();
let person_id = 1234;
// probably not the best design choice to duplicate the person_id,
// for the purpose here is not important
db.add_person(person_id, Person::new(person_id, "Bob"));
db_update!(db #person_id => set_name("Gambadilegno"));
println!("your new name is {}", db.get_person(person_id).unwrap().name);
}
我认为的宏的格式为:
<database_instance> #<object_key> => <method_name>(<args>)
在宏实现和完整的演示代码下面:
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
macro_rules! db_update {
($db:ident # $id:expr => $meth:tt($($args:tt)*)) => {
$db.modify_person($id, |person| {
person.unwrap().$meth($($args)*);
});
};
}
#[derive(Clone)]
struct Person {
id: u64,
name: String,
}
impl Person {
pub fn new(id: u64, name: &str) -> Person {
Person {
id: id,
name: name.to_string(),
}
}
fn set_name(&mut self, value: &str) {
self.name = value.to_string();
}
}
struct Database {
db: Arc<Mutex<HashMap<u64, Person>>>, // access from different threads
}
impl Database {
pub fn new() -> Database {
Database {
db: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn add_person(&self, id: u64, person: Person) {
self.db.lock().unwrap().insert(id, person);
}
pub fn modify_person<F>(&self, id: u64, f: F)
where
F: FnOnce(Option<&mut Person>),
{
f(self.db.lock().unwrap().get_mut(&id));
}
pub fn get_person(&self, id: u64) -> Option<Person> {
self.db.lock().unwrap().get(&id).cloned()
}
}