在内存数据库设计中

cro*_*cus 3 rust

我正在尝试使用创建内存数据库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();
    }
}
Run Code Online (Sandbox Code Playgroud)

我有结构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)
    }
}
Run Code Online (Sandbox Code Playgroud)

和使用此数据库的代码:

let mut db = Database::new();
db.add_person(1, Person::new(1, "Bob"));
Run Code Online (Sandbox Code Playgroud)

我想改person名字:

let mut person = db.get_person(1).unwrap();
person.set_name("Bill");
Run Code Online (Sandbox Code Playgroud)

Rust操场上完整代码.

在编译时,我遇到了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 | |     }
   | |_____^
Run Code Online (Sandbox Code Playgroud)

如何实施这种方法?

use*_*342 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
Run Code Online (Sandbox Code Playgroud)

要更正代码,您需要调整设计以满足以下约束:

  • 不允许任何引用超过引用对象;
  • 在可变引用的生命周期中,可能不存在对象的其他引用(可变或不可变).

add_person方法已经符合这两个规则,因为它会占用您传递它的对象,并将其移动到数据库中.

如果我们修改get_person()为返回不可变引用怎么办?

pub fn get_person(&self, id: i64) -> Option<&Person> {
    self.db.lock().unwrap().get(&id)
}
Run Code Online (Sandbox Code Playgroud)

即使这个看似无辜的版本仍然无法编译!那是因为它违反了第一条规则.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()
}
Run Code Online (Sandbox Code Playgroud)

这种更改不适用于其他用例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
    }
}
Run Code Online (Sandbox Code Playgroud)

随着字段数量的Person增加,这将很快变得乏味.它也可能变慢,因为每次访问都必须获取一个单独的互斥锁.

幸运的是,有一种更好的方法来实现对条目的修改.请记住,使用可变引用违反了规则,除非 Rust可以证明引用不会"逃避"使用它的块.这可以通过反相控制来保证-而不是get_person()返回易变的参考,我们可以引入一个modify_person()通过将可变引用到一个可调用的,它可以做任何事情与它喜欢.例如:

pub fn modify_person<F>(&self, id: i64, f: F) where F: FnOnce(Option<&mut Person>) {
    f(self.db.lock().unwrap().get_mut(&id))
}
Run Code Online (Sandbox Code Playgroud)

用法如下所示:

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");
    });
}
Run Code Online (Sandbox Code Playgroud)

最后,如果你担心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))
}
Run Code Online (Sandbox Code Playgroud)

除了获取共享引用之外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());
Run Code Online (Sandbox Code Playgroud)