Як створити літерал HashMap?


87

Як я можу створити літерал HashMap у Rust? У Python я можу це зробити так:

hashmap = {
   'element0': {
       'name': 'My New Element',
       'childs': {
           'child0': {
               'name': 'Child For Element 0',
               'childs': {
                   ...
               }
           }
       }
   },
   ...
}

А в Go наступним чином:

type Node struct {
    name string
    childs map[string]Node
}

hashmap := map[string]Node {
    "element0": Node{
        "My New Element",
        map[string]Node {
            'child0': Node{
                "Child For Element 0",
                map[string]Node {}
            }
        }
    }
}

Відповіді:


90

У Rust немає синтаксису буквального відображення на карті. Я не знаю точної причини, але я сподіваюся, що той факт, що існує безліч структур даних, які діють як карта (наприклад, обидва BTreeMapта HashMap), ускладнить вибір такої.

Тим не менш, ви можете створити макрос, який виконуватиме роботу за вас, як показано в Чому цей макрос HashMap іржі більше не працює? . Ось цей макрос дещо спрощений і має достатню структуру, щоб зробити його доступним на ігровому майданчику :

macro_rules! map(
    { $($key:expr => $value:expr),+ } => {
        {
            let mut m = ::std::collections::HashMap::new();
            $(
                m.insert($key, $value);
            )+
            m
        }
     };
);

fn main() {
    let names = map!{ 1 => "one", 2 => "two" };
    println!("{} -> {:?}", 1, names.get(&1));
    println!("{} -> {:?}", 10, names.get(&10));
}

Цей макрос уникає виділення непотрібного проміжного продукту Vec, але він не використовує, HashMap::with_capacityтому можуть бути деякі марні перерозподіли HashMapзначень як додані значення. Можлива більш складна версія макросу, яка підраховує значення, але переваги продуктивності, мабуть, не те, що виграє більшість видів використання макроса.


У нічній версії Rust ви можете уникнути як непотрібного розподілу (і перерозподілу!), Так і необхідності в макросі:

#![feature(array_value_iter)]

use std::array::IntoIter;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::iter::FromIterator;

fn main() {
    let s = Vec::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = BTreeSet::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = HashSet::<_>::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = BTreeMap::from_iter(IntoIter::new([(1, 2), (3, 4)]));
    println!("{:?}", s);

    let s = HashMap::<_, _>::from_iter(IntoIter::new([(1, 2), (3, 4)]));
    println!("{:?}", s);
}

Потім цю логіку можна також обернути назад у макрос:

#![feature(array_value_iter)]

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

macro_rules! collection {
    // map-like
    ($($k:expr => $v:expr),* $(,)?) => {
        std::iter::Iterator::collect(std::array::IntoIter::new([$(($k, $v),)*]))
    };
    // set-like
    ($($v:expr),* $(,)?) => {
        std::iter::Iterator::collect(std::array::IntoIter::new([$($v,)*]))
    };
}

fn main() {
    let s: Vec<_> = collection![1, 2, 3];
    println!("{:?}", s);

    let s: BTreeSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: HashSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: BTreeMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);

    let s: HashMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);
}

Дивитися також:


6
У grabbag_macrosящику також є одна . Ви можете побачити джерело тут: github.com/DanielKeep/rust-grabbag/blob/master/grabbag_macros/… .
ДК.

4
Ах, соромно. Мені подобається підхід Swift до цього, який абстрагує літерали від їх типів. A DictionaryLiteralможе бути використаний для ініціалізації будь-якого типу, який відповідає ExpressibleByDictionaryLiteral(хоча стандартна бібліотека пропонує один такий тип, Dictionary)
Олександр

48

Я рекомендую ящик підсвічуванням .

Цитата з документації:

Макроси для літералів-контейнерів із певним типом.

use maplit::hashmap;

let map = hashmap!{
    "a" => 1,
    "b" => 2,
};

Ящик, =>що відображається на карті, використовує синтаксис для макросів відображення. Не можна використовувати: як роздільник через синтаксичні обмеження в звичайних macro_rules!макросах.

Зверніть увагу, що макроси іржі гнучкі, в дужках яких ви використовуєте для виклику. Ви можете використовувати їх як hashmap!{}або hashmap![]або hashmap!(). Цей ящик передбачає, що, {}як правило, для &макросів набору карт , він відповідає їх виводу налагодження.

Макроси

  • btreemap Створіть a BTreeMapзі списку пар ключ-значення
  • btreeset Створіть a BTreeSetзі списку елементів.
  • hashmap Створіть a HashMapзі списку пар ключ-значення
  • hashset Створіть a HashSetзі списку елементів.

42

Є приклад того, як цього досягти, в документації дляHashMap :

let timber_resources: HashMap<&str, i32> = [("Norway", 100), ("Denmark", 50), ("Iceland", 10)]
    .iter()
    .cloned()
    .collect();

4
Хоча це працює в цьому випадку, воно стає непотрібним, якщо використовувати щось на зразок Stringзамість &str.
Шепмастер

7
Звичайно, є витрати на час роботи, але є також витрати, пов’язані з додаванням іншої залежності ящика або з визначенням важко зрозумілих макросів. Більшість кодів не є критично важливими для продуктивності, і я вважаю цю версію дуже читабельною.
jupp0r

2
Крім того, це не працює для типів, які не є клонуванням.
Хатч Мур

10
Цей приклад можна налаштувати, щоб уникнути цих проблем, використовуючиvec![ (name, value) ].into_iter().collect()
Йоганнес

1
@Stargateur, тому що я не займаюся передчасною оптимізацією, а також ви не повинні. Можливо, вашій програмі виграє більш оптимізована ініціалізація літералу HashMap, але, ймовірно, це не буде. Якщо ви хочете отримати найкращу продуктивність, виміряйте та оптимізуйте ті частини програми, які знаходяться на критичному шляху. Випадкова оптимізація матеріалів - це просто погана стратегія. Моя відповідь буде гарна для 99,9999% людей. Це читабельно, швидко компілюється, не створює залежностей, що збільшують складність та створюють потенційні уразливості безпеки.
jupp0r

4

Як зазначив @Johannes у коментарях, це можна використовувати, vec![]оскільки:

  • Vec<T>реалізує IntoIterator<T>рису
  • HashMap<K, V> знаряддя праці FromIterator<Item = (K, V)>

це означає, що ви можете зробити це:

let map: HashMap<String, String> = vec![("key".to_string(), "value".to_string())]
    .into_iter()
    .collect();

Ви можете використовувати, &strале вам може знадобитися анотувати життя, якщо це не так 'static:

let map: HashMap<&str, usize> = vec![("one", 1), ("two", 2)].into_iter().collect();

Чи не буде це представляти векторний літерал у двійковому файлі, а потім збирати його у хеш-карту під час виконання?
alex elias

1

Ви можете використовувати velcroящик *. Це схоже наmaplit , що рекомендується в інших відповідях, але з більшою кількістю типів колекцій, кращим синтаксисом (принаймні на мій погляд!) Та більшою кількістю функцій.

Якщо припустити, що ви хочете використовувати Strings, а не &str, ваш точний приклад буде виглядати так:

use std::collections::HashMap;
use velcro::hash_map;

struct Node {
    name: String
    children: HashMap<String, Node>,
}

let map = hash_map! {
    String::from("element0"): Node {
        name: "My New Element".into(),
        children: hash_map! {
            String::from("child0"): Node {
                name: "child0".into(),
                children: hash_map!{}
            }
        }
    }
};

Це трохи потворно через те String, як побудовано s. Але його можна зробити трохи чистішим, не змінюючи типу ключа, за допомогою hash_map_from!якого автоматично буде здійснено перетворення:

use std::collections::HashMap;
use velcro::{hash_map, hash_map_from};

let map: HashMap<String, Node> = hash_map_from! {
    "element0": Node {
        name: "My New Element".into(),
        children: hash_map_from! {
            "child0": Node {
                name: "child0".into(),
                children: hash_map!{}
            }
        }
    }
};

Що не набагато детальніше, ніж версія Go.


* Повне розкриття інформації: Я автор цього ящика.


0

За один елемент

Якщо ви хочете ініціалізувати карту лише одним елементом в одному рядку (і без видимої мутації у вашому коді), ви можете зробити:

let map: HashMap<&'static str, u32> = Some(("answer", 42)).into_iter().collect();

Це завдяки корисності Optionможливість стати Iteratorвикористанням into_iter().

У реальному коді вам, мабуть, не потрібно допомагати компілятору з типом:

use std::collections::HashMap;

fn john_wick() -> HashMap<&'static str, u32> {
    Some(("answer", 42)).into_iter().collect()
}

fn main() {
    let result = john_wick();

    let mut expected = HashMap::new();
    expected.insert("answer", 42);

    assert_eq!(result, expected);
}

Існує також спосіб прив’язати це до того, щоб більше ніж один елемент робив щось подібне Some(a).into_iter().chain(Some(b).into_iter()).collect(), але це довше, менш читається і, можливо, має деякі проблеми з оптимізацією, тому я раджу його не використовувати.


-2

Я бачив купу вигадливих рішень, але я просто хотів чогось простого. З цією метою є ознака:

use std::collections::HashMap;

trait Hash {
   fn to_map(&self) -> HashMap<&str, u16>;
}

impl Hash for [(&str, u16)] {
   fn to_map(&self) -> HashMap<&str, u16> {
      self.iter().cloned().collect()
   }
}

fn main() {
   let m = [("year", 2019), ("month", 12)].to_map();
   println!("{:?}", m)
}

Я вважаю, що це хороший варіант, оскільки це, по суті, те, що вже використовується Рубі та Німом:


2
"Я думаю, що це хороший варіант, оскільки це, по суті, те, що вже використовували Рубі та Нім". Я не розумію, як це аргумент? чому Рубін або Нім роблять це, був би кращим аргументом, аніж просто "вони це роблять, тож це добре"
Stargateur

думка без аргументів не корисна у відповіді SO
Stargateur

2
Деталь: називати ознаку "хеш" - погана ідея: вже існує ознака з такою назвою, і ви швидко зіткнетеся з нею при роботі з хешуванням в іржі.
Denys Séguret

2
Я б також порадив copied()заборонити забороняти тип, який не реалізує копію, уникаючи клонування нічого, або, принаймні, робити це явним, використовуючи cloned_to_map (), якщо ви також хочете мати можливість робити це лише з клонованим типом.
Stargateur
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.