¿Cómo creo un literal HashMap?

87

¿Cómo puedo crear un literal HashMap en Rust? En Python puedo hacerlo así:

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

Y en Go así:

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
fuente

Respuestas:

90

No hay una sintaxis literal de mapa en Rust. No sé la razón exacta , pero espero que el hecho de que haya múltiples estructuras de datos que actúan como un mapa (como ambos BTreeMapy HashMap) dificultaría la elección de una.

Sin embargo, puede crear una macro para que haga el trabajo por usted, como se demuestra en ¿Por qué esta macro HashMap de óxido ya no funciona? . Aquí está esa macro simplificada un poco y con suficiente estructura para que se pueda ejecutar en el patio de recreo :

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

Esta macro evita la asignación de un intermedio innecesario Vec, pero no se usa, HashMap::with_capacitypor lo que puede haber algunas reasignaciones inútiles de los HashMapvalores a medida que se agregan. Es posible una versión más complicada de la macro que cuenta los valores, pero los beneficios de rendimiento probablemente no sean algo de lo que la mayoría de los usos de la macro se beneficiarían.


En una versión nocturna de Rust, puede evitar tanto la asignación innecesaria (¡y la reasignación!) Como la necesidad de una macro:

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

Esta lógica también se puede volver a envolver en una macro:

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

Ver también:

Pastor
fuente
6
También hay uno en la grabbag_macroscaja. Puede ver la fuente aquí: github.com/DanielKeep/rust-grabbag/blob/master/grabbag_macros/… .
DK.
4
Ah, vergüenza. Me gusta el enfoque Swift para esto, que abstrae los literales de sus tipos. A DictionaryLiteralse puede usar para inicializar cualquier tipo que se ajuste a ExpressibleByDictionaryLiteral(aunque la biblioteca estándar ofrece uno de esos tipos Dictionary)
Alexander
48

Recomiendo la caja maplit .

Para citar de la documentación:

Macros para literales de contenedor con un tipo específico.

use maplit::hashmap;

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

La caja maplit utiliza la =>sintaxis para las macros de mapeo. No es posible utilizarlo :como separador debido a las restricciones sintácticas de las macro_rules!macros normales .

Tenga en cuenta que las macros de rust son flexibles en cuanto a los corchetes que utiliza para la invocación. Puede usarlos como hashmap!{}o hashmap![]o hashmap!(). Esta caja sugiere que {}como convención para las &macros de conjuntos de mapas , coincide con su salida de depuración.

Macros

  • btreemap Crear a BTreeMappartir de una lista de pares clave-valor
  • btreeset Cree un a BTreeSetpartir de una lista de elementos.
  • hashmap Crear a HashMappartir de una lista de pares clave-valor
  • hashset Cree un a HashSetpartir de una lista de elementos.
David J.
fuente
42

Hay un ejemplo de cómo lograr esto en la documentación paraHashMap :

let timber_resources: HashMap<&str, i32> = [("Norway", 100), ("Denmark", 50), ("Iceland", 10)]
    .iter()
    .cloned()
    .collect();
jupp0r
fuente
4
Si bien funciona en este caso, se vuelve innecesariamente costoso cuando se usa algo como en Stringlugar de &str.
Shepmaster
7
Seguro que hay costos de tiempo de ejecución, pero también hay costos asociados con la adición de otra dependencia de caja o con la definición de macros difíciles de entender. La mayor parte del código no es crítico para el rendimiento y encuentro esta versión muy legible.
jupp0r
2
Además, esto no funciona para tipos que no son Clon.
Hutch Moore
10
Este ejemplo se puede ajustar para evitar esos problemas utilizandovec![ (name, value) ].into_iter().collect()
Johannes
1
@Stargateur porque no hago optimización prematura y tú tampoco deberías. Puede ser que su programa se beneficie de una inicialización literal de HashMap más optimizada, pero probablemente no sea así. Si desea obtener el mejor rendimiento, mida y optimice las partes de su programa que se encuentran en la ruta crítica. Optimizar cosas al azar es solo una mala estrategia. Mi respuesta estará bien para el 99,9999% de las personas. Es legible, se compila rápido, no introduce dependencias que aumenten la complejidad e introduzcan posibles vulnerabilidades de seguridad.
jupp0r
4

Como señaló @Johannes en los comentarios, es posible usarlo vec![]porque:

  • Vec<T>implementa el IntoIterator<T>rasgo
  • HashMap<K, V> implementos FromIterator<Item = (K, V)>

lo que significa que puedes hacer esto:

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

Puede usar, &strpero es posible que deba anotar vidas útiles si no lo es 'static:

let map: HashMap<&str, usize> = vec![("one", 1), ("two", 2)].into_iter().collect();
Kamil Tomšík
fuente
¿No representará esto el vector literal en el binario y luego lo recopilará en un mapa hash en tiempo de ejecución?
alex elias
1

Puede utilizar la velcrocaja *. Esto es similar amaplit , como se recomienda en otras respuestas, pero con más tipos de colección, mejor sintaxis (¡al menos en mi opinión!) Y más funciones.

Suponiendo que desea usar Strings en lugar de &str, su ejemplo exacto se vería así:

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

Eso es un poco feo por cómo String se construyen los s. Pero se puede hacer un poco más limpio, sin cambiar el tipo de clave, usando hash_map_from!que automáticamente hará conversiones:

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

Que no es mucho más detallado que la versión Go.


* Divulgación completa: soy el autor de esta caja.

Peter Hall
fuente
0

Por un elemento

Si desea inicializar el mapa con un solo elemento en una línea (y sin una mutación visible en su código), puede hacer:

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

Esto es gracias a la utilidad de Optionpoder convertirse en un Iteratorusuariointo_iter() .

En código real, probablemente no necesite ayudar al compilador con el tipo:

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

También hay una manera de encadenar esto para tener más de un elemento haciendo algo así Some(a).into_iter().chain(Some(b).into_iter()).collect(), pero esto es más largo, menos legible y probablemente tenga algunos problemas de optimización, así que lo desaconsejo.

Stargateur
fuente
-2

He visto un montón de soluciones sofisticadas, pero solo quería algo simple. Con ese fin, aquí hay un rasgo:

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

Creo que es una buena opción, ya que es esencialmente lo que ya utilizan Ruby y Nim:

Steven Penny
fuente
2
"Creo que es una buena opción, ya que es esencialmente lo que ya utilizan Ruby y Nim" No entiendo cómo es esto un argumento. por qué Ruby o Nim hacen esto sería un mejor argumento que simplemente "lo hacen así que es bueno"
Stargateur
una opinión sin argumento no es útil en una respuesta SO
Stargateur
2
Un detalle: llamar al rasgo "Hash" es una mala idea: ya hay un rasgo con ese nombre y lo encontrarás rápidamente cuando trabajes con hash en rust.
Denys Séguret
2
También recomendaría preferir copied()no permitir el tipo que no implementa la copia, evitando la clonación por nada, o al menos hacerlo explícito usando un cloned_to_map () si también desea poder hacerlo con solo el tipo clonado.
Stargateur