Вставить в произвольный вложенный HashMap - PullRequest
0 голосов
/ 18 января 2020

Я хочу иметь структуру данных, которая позволяет мне иметь произвольный вложенный HashMap. Для этого я построил следующую структуру:

struct Database {
    children: HashMap<String, Database>,
    data: String,
}

Для вставки в эту структуру я получаю список ключей и значение для вставки. Так, например, для ввода

let subkeys = vec!["key1", "key1.1", "key1.1.3"];
let value = "myvalue";

я хочу, чтобы база данных имела такую ​​(псевдо) структуру:

{
    "data" : "",
    "children": {
        "key1": {
            "data" : "",
            "children": {
                "key1.1": {
                    "data" : "",
                    "children" : {
                        "key1.1.3": {
                            "data": "myvalue",
                            "children" : {}
                        }
                    }  
                }
            }
        }
    }   
}

, а затем, например, для второго запроса вставки

let subkeys = vec!["key1", "key1.1", "key1.1.2"];
let value = "myvalue2";

структура должна выглядеть (псевдо) так:

{
    "data" : "",
    "children": {
        "key1": {
            "data" : "",
            "children": {
                "key1.1": {
                    "data" : "",
                    "children" : {
                        "key1.1.3": {
                            "data": "myvalue",
                            "children" : {}
                        },
                        "key1.1.2": {
                            "data": "myvalue2",
                            "children" : {}
                        }
                    }  
                }
            }
        }
    }   
}

Итак, вот минимальный воспроизводимый пример того, что я пробовал (не работает) детская площадка

use std::collections::HashMap;

struct Database {
    children: HashMap<String, Database>,
    data: String,
}

fn main()
{
    // make a databse object
    let mut db = Database { 
        children: HashMap::new(),
        data: "root".to_string(), 
    };

    // some example subkeys
    let subkeys = vec!["key1", "key1.1", "key1.1.3"];
    // and the value i want to insert
    let value = "myvalue";

    // a reference to the current HashMap
    // initialize with the root 
    let mut root = &db.children;

    // iterate throught subkeys
    for subkey in subkeys.iter() {

        // match the result of a get request to the hashmap
        match root.get::<String>(&subkey.to_string()) {
            Some(child) => {
                // if the key already exists set the root to the children of the child
                root = &child.children;
            }
            None => {
                // if key doesnt exist add it with a ne empty hashmap
                let d = Database{children: HashMap::new(), data: "".to_string()};

                // set root to this new databse obejct
                root = &d.children; 

                root.insert(subkey.to_string(), d);


            }
        }
    }
}

Итак, насколько я понимаю, существуют проблемы с этим кодом:

  1. &d.children сбрасывается после типа match и так root " "не имеет значения

  2. , а также root.insert представляется проблемой, поскольку root является ссылкой &, поэтому данные, на которые он ссылается, не могут быть заимствованы как изменяемые`

Что мне нужно сделать, чтобы мой код работал и давал результаты, как показано выше. Возможно, мне нужно что-то изменить в моем struct Database?

1 Ответ

2 голосов
/ 18 января 2020

Сначала несколько комментариев о том, что у вас есть и почему это не работает. root должен быть изменяемой ссылкой. Обратите внимание на различие между изменяемой переменной (let mut root = &db.children;) и изменяемой ссылкой (let root = &mut db.children;). Первый позволяет изменять саму переменную. Последнее позволяет изменять данные за ссылкой. В этом случае нам нужны оба (let mut root = &mut db.children), потому что мы не только изменяем root при выполнении итерации по узлам, но также изменяем данные за ссылкой, когда нам нужно вставить новый узел.

То же самое относится к d во внутренней l oop (это должна быть изменяемая переменная), хотя, как мы увидим, мутирование d не совсем то, что мы хотим.

// if key doesnt exist add it with a ne empty hashmap
let d = Database{children: HashMap::new(), data: "".to_string()};

// set root to this new databse obejct
root = &mut d.children; 

root.insert(subkey.to_string(), d);

На мгновение игнорируя ошибки, что должен этот код делать? d - это новый Database без реальных данных. Затем мы устанавливаем root как (пустой) набор дочерних элементов этого нового Database. Наконец, мы вставляем новый Database в root. Но поскольку мы изменили root на втором шаге, он больше не является родительским: мы вставляем d как дочерний элемент!

Вместо этого мы хотим изменить порядок вторых двух шагов. Но если мы просто переключим эти две строки, мы получим ошибку

error[E0382]: borrow of moved value: `d`
  --> src/main.rs:41:24
   |
36 |                 let mut d = Database{children: HashMap::new(), data: "".to_string()};
   |                     ----- move occurs because `d` has type `Database`, which does not implement the `Copy` trait
37 |                 
38 |                 root.insert(subkey.to_string(), d);
   |                                                 - value moved here
...
41 |                 root = &mut d.children; 
   |                        ^^^^^^^^^^^^^^^ value borrowed here after move

Так что проблема в том, что d больше не является локальной переменной, когда мы пытаемся установить root для ее дочерних элементов. Нам нужно root, чтобы быть потомками только что вставленного значения. Обычная идиома для такого рода вещей - entry API . Это позволяет нам пытаться получить значение из HashMap и, если оно не найдено, вставить что-то. Наиболее уместно, что эта вставка возвращает изменяемую ссылку на любое значение, которое теперь находится в этом ключе.

Теперь этот раздел выглядит как

// if key doesnt exist add it with a new empty hashmap
let d = Database{children: HashMap::new(), data: "".to_string()};

// insert the new database object and
// set root to the hashmap of children
root = &mut root.entry(subkey.to_string()).or_insert(d).children;

На данный момент у нас есть явно работающая программа . Добавив #[derive(Debug)] к Database, мы можем увидеть, как выглядит база данных с println!("{:#?}, db);. Однако, если мы попытаемся добавить второе значение, все взрывается. Вместо того чтобы размещать два значения рядом, они оказываются в совершенно разных ветвях базы данных. Это восходит к закомментированным строкам в ветке Some(child) оператора match.

Мы бы хотели установить root в изменяемую ссылку на child.children, но даже просто раскомментировать эту строку без любые изменения приводят к ошибке, что root является заимствованным заимствованным, в то время как заимствовано в другом месте. Проблема в том, что мы используем заем в root.get(&subkey.to_string()) сейчас. Раньше, поскольку мы игнорировали child, а другая ветвь не использовала никаких данных из этого заимствования, заимствование могло закончиться немедленно. Теперь это должно длиться на протяжении всего матча. Это не позволяет нам брать заимствования даже в случае None.

К счастью, поскольку мы используем entry API, нам вообще не нужен этот оператор сопоставления! Все это можно просто заменить на

let d = Database {
    children: HashMap::new(),
    data: "".to_string(),
};

// insert the new database object and
// set root to the hashmap of children
root = &mut root.entry(subkey.to_string()).or_insert(d).children;

Если подраздел уже существует в наборе дочерних элементов, root.entry(...).or_insert(...) будет указывать на этого уже существующего дочернего элемента.

Теперь нам просто нужно очистить код. Так как вы используете его более одного раза, я бы порекомендовал учесть факт вставки пути клавиш в функцию. Вместо того, чтобы следовать по пути HashMap<String, Database>, я бы рекомендовал следовать самому Database, поскольку это позволит вам изменить его поле data в конце. Для этого я бы предложил функцию с такой подписью:

impl Database {
    fn insert_path(&mut self, path: &[&str]) -> &mut Database {
        todo!()
    }
}

Далее, поскольку нам нужно создать новый Database (d) только тогда, когда он еще не существует, мы может использовать Entry метод or_insert_with для создания новой базы данных только при необходимости. Это проще всего, когда есть функция для создания новой базы данных, поэтому давайте добавим #[derive(Default)] в список производных на Database. Это делает нашу функцию

impl Database {
    fn insert_path(&mut self, path: &[&str]) -> &mut Self {
        let mut root = self;
        // iterate throught path
        for subkey in path.iter() {
            // insert the new database object if necessary and
            // set root to the hashmap of children
            root = root
                .children
                .entry(subkey.to_string())
                // insert (if necessary) using the Database::default method
                .or_insert_with(Database::default);
        }
        root
    }
}

На этом этапе мы должны запустить cargo clippy, чтобы увидеть, есть ли какие-либо предложения. Есть один способ использования to_string на &&str. Чтобы это исправить, у вас есть два варианта. Во-первых, используйте один из других методов для преобразования &str с String с вместо to_string. Во-вторых, разыменуйте &&str перед использованием to_string. Этот второй вариант проще. Поскольку мы перебираем &[&str] (Vec<&str>::iter в вашем оригинале), элементы в итерации равны &&str. Идиоматический c способ избавиться от дополнительного слоя ссылок - использовать шаблон для деструктурирования элементов.

for &subkey in path {
   ^^^ this is new
    ... // subkey has type &str instead of &&str here
}

Мой последний совет - изменить имя root на что-то более универсальный c, как node. Это только root в самом начале, поэтому имя вводит в заблуждение после этого. Вот окончательный код вместе с вашими тестами (детская площадка) :

use std::collections::HashMap;

#[derive(Default, Debug)]
struct Database {
    children: HashMap<String, Database>,
    data: String,
}

impl Database {
    fn insert_path(&mut self, path: &[&str]) -> &mut Self {
        // node is a mutable reference to the current database
        let mut node = self;
        // iterate through the path
        for &subkey in path.iter() {
            // insert the new database object if necessary and
            // set node to (a mutable reference to) the child node
            node = node
                .children
                .entry(subkey.to_string())
                .or_insert_with(Database::default);
        }
        node
    }
}

fn main() {
    // make a databse object
    let mut db = Database {
        children: HashMap::new(),
        data: "root".to_string(),
    };

    // some example subkeys
    let subkeys = vec!["key1", "key1.1", "key1.1.3"];
    // and the value i want to insert
    let value = "myvalue";

    let node = db.insert_path(&subkeys);
    node.data = value.to_string();

    println!("{:#?}", db);

    let subkeys = vec!["key1", "key1.1", "key1.1.2"];
    let value = "myvalue2";

    let node = db.insert_path(&subkeys);
    node.data = value.to_string();

    println!("{:#?}", db);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...