Jak utworzyć literał HashMap?

87

Jak mogę utworzyć literał HashMap w Rust? W Pythonie mogę to zrobić tak:

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

A w Go w ten sposób:

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 {}
            }
        }
    }
}
Maxim Samburskiy
źródło

Odpowiedzi:

90

W Rust nie ma składni literału mapy. Nie znam dokładnego powodu, ale spodziewam się, że fakt, że istnieje wiele struktur danych, które działają jak mapy (takie jak oba BTreeMapi HashMap), utrudniłby wybranie jednej.

Możesz jednak utworzyć makro, które wykona zadanie za Ciebie, jak pokazano w artykule Dlaczego to rdzawe makro HashMap już nie działa? . Oto makro nieco uproszczone i posiadające wystarczającą strukturę, aby można było je uruchomić na placu zabaw :

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));
}

To makro pozwala uniknąć przydzielania niepotrzebnego elementu pośredniego Vec, ale nie używa, HashMap::with_capacitywięc mogą wystąpić niepotrzebne realokacje w HashMapmiarę dodawania wartości. Bardziej skomplikowana wersja makra zliczającego wartości jest możliwa, ale korzyści w zakresie wydajności prawdopodobnie nie są czymś, na czym większość zastosowań tego makra skorzystałaby.


W nocnej wersji Rusta można uniknąć zarówno niepotrzebnej alokacji (i realokacji!), Jak i konieczności stosowania makra:

#![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);
}

Ta logika może być następnie zawinięta z powrotem w makro:

#![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);
}

Zobacz też:

Shepmaster
źródło
6
Jest też jeden w grabbag_macrosskrzyni. Możesz zobaczyć źródło tutaj: github.com/DanielKeep/rust-grabbag/blob/master/grabbag_macros/… .
DK.
4
Ach, szkoda. Podoba mi się szybkie podejście do tego, które oddziela literały od ich typów. A DictionaryLiteralmoże być użyty do zainicjowania dowolnego typu, który jest zgodny z ExpressibleByDictionaryLiteral(nawet jeśli standardowa biblioteka oferuje jeden taki typ Dictionary)
Alexander
48

Polecam skrzynkę podświetlaną na mapę .

Cytat z dokumentacji:

Makra dla literałów kontenera z określonym typem.

use maplit::hashmap;

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

Maplit crate używa =>składni do mapowania makr. Nie jest możliwe użycie :jako separatora ze względu na ograniczenia składniowe w zwykłych macro_rules!makrach.

Zwróć uwagę, że makra rdzy są elastyczne, w których nawiasach używasz do wywołania. Możesz ich używać jako hashmap!{}lub hashmap![]lub hashmap!(). Ta skrzynka sugeruje {}jako konwencję &makr zestawu map , jest zgodna z ich wyjściem debugowania.

Makra

  • btreemap Utwórz BTreeMapz listy par klucz-wartość
  • btreeset Utwórz BTreeSetz listy elementów.
  • hashmap Utwórz HashMapz listy par klucz-wartość
  • hashset Utwórz HashSetz listy elementów.
David J.
źródło
42

Przykład, jak to osiągnąć, znajduje się w dokumentacjiHashMap :

let timber_resources: HashMap<&str, i32> = [("Norway", 100), ("Denmark", 50), ("Iceland", 10)]
    .iter()
    .cloned()
    .collect();
jupp0r
źródło
4
Chociaż działa w tym przypadku, staje się niepotrzebnie kosztowne, gdy używa się czegoś takiego Stringzamiast &str.
Shepmaster
7
Oczywiście, istnieją koszty działania, ale są też koszty związane z dodaniem kolejnej zależności od skrzynki lub zdefiniowaniem trudnych do zrozumienia makr. Większość kodu nie ma krytycznego znaczenia dla wydajności i uważam tę wersję za bardzo czytelną.
jupp0r
2
Ponadto nie działa to w przypadku typów, które nie są klonem.
Hutch Moore
10
Ten przykład można dostroić, aby uniknąć tych problemów, używającvec![ (name, value) ].into_iter().collect()
Johannes,
1
@Stargateur, ponieważ nie robię przedwczesnej optymalizacji i Ty też nie powinieneś. Może się zdarzyć, że Twój program skorzystałby na bardziej zoptymalizowanej inicjalizacji dosłownej HashMap, ale prawdopodobnie nie. Jeśli chcesz uzyskać najlepszą wydajność, mierz i optymalizuj części programu, które znajdują się na ścieżce krytycznej. Losowa optymalizacja rzeczy to po prostu zła strategia. Moja odpowiedź będzie odpowiednia dla 99,9999% ludzi. Jest czytelny, szybko się kompiluje, nie wprowadza zależności, które zwiększają złożoność i wprowadzają potencjalne luki w zabezpieczeniach.
jupp0r
4

Jak zauważył @Johannes w komentarzach, można go używać, vec![]ponieważ:

  • Vec<T>realizuje IntoIterator<T>cechę
  • HashMap<K, V> przybory FromIterator<Item = (K, V)>

co oznacza, że ​​możesz to zrobić:

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

Możesz użyć, &strale może być konieczne dodanie adnotacji, jeśli nie jest 'static:

let map: HashMap<&str, usize> = vec![("one", 1), ("two", 2)].into_iter().collect();
Kamil Tomšík
źródło
Czy to nie będzie reprezentować literału wektorowego w pliku binarnym, a następnie zebrać go w hashmap w czasie wykonywania?
alex elias
1

Możesz skorzystać ze velcroskrzynki *. Jest podobny do tego maplit, co jest zalecane w innych odpowiedziach, ale z większą liczbą typów kolekcji, lepszą składnią (przynajmniej moim zdaniem!) I większą liczbą funkcji.

Zakładając, że chcesz użyć Strings zamiast &str, dokładny przykład wyglądałby tak:

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!{}
            }
        }
    }
};

To trochę brzydkie ze względu na to, jak Stringsą zbudowane. Ale można to uczynić nieco czystszym, bez zmiany typu klucza, używając, hash_map_from!które automatycznie wykona konwersje:

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!{}
            }
        }
    }
};

Co nie jest dużo bardziej szczegółowe niż wersja Go.


* Pełne ujawnienie: jestem autorem tej skrzynki.

Peter Hall
źródło
0

Na jeden element

Jeśli chcesz zainicjować mapę tylko z jednym elementem w jednej linii (i bez widocznej mutacji w kodzie), możesz zrobić:

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

Jest to możliwe dzięki przydatności Optionjest w stanie stać się Iteratorużywając into_iter().

W prawdziwym kodzie prawdopodobnie nie musisz pomagać kompilatorowi z typem:

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);
}

Istnieje również sposób na połączenie tego w łańcuch, aby więcej niż jeden element robił coś podobnego Some(a).into_iter().chain(Some(b).into_iter()).collect(), ale jest dłuższy, mniej czytelny i prawdopodobnie ma pewne problemy z optymalizacją, więc odradzam to.

Stargateur
źródło
-2

Widziałem kilka wymyślnych rozwiązań, ale chciałem tylko czegoś prostego. W tym celu oto cecha:

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)
}

Myślę, że to dobra opcja, ponieważ zasadniczo jest to coś, czego używają już Ruby i Nim:

Steven Penny
źródło
2
„Myślę, że to dobra opcja, ponieważ zasadniczo jest to coś, czego już używali Ruby i Nim”. Nie rozumiem, dlaczego to jest argument? dlaczego ruby ​​lub Nim robią to, byłoby lepszym argumentem niż po prostu „robią to, więc jest dobrze”
Stargateur,
opinia bez argumentów nie jest przydatna w odpowiedzi SO
Stargateur
2
Szczegół: nazywanie cechy „Hash” to zły pomysł: cecha o tej nazwie już istnieje i szybko ją napotkasz podczas pracy z haszowaniem rdzy.
Denys Séguret
2
Chciałbym również rada wolą copied(), aby uniemożliwić typu, które nie wdrażają kopię, unikając klonowanie za nic, albo przynajmniej uczynić go jednoznacznie za pomocą cloned_to_map (), jeśli chcesz, aby móc to zrobić tylko ze sklonowanego typu.
Stargateur