Ya revisamos los vectores y los Strings. Ahora toca el turno de los Hash Maps en Rust. La definición del tipo es HashMap<K, V>
. Este tipo de colección nos es útil cuando queremos acceder a los datos sin usar un index
sino una clave, que puede ser de cualquier tipo.
Los hash maps en Rust funcionan de manera similar a un diccionario, donde las claves son únicas y se utilizan para acceder a los valores asociados. Esto lo vemos mucho en respuestas a peticiones de API Rest con el formato JSON
.
Hash Maps en Rust
Crear un hash map en Rust
Crearemos un hash map vacío.
use std::collections::HashMap;
let mut spanish = HashMap::new();
spanish.insert(String::from("hi"), String::from("hola"));
spanish.insert(String::from("bye"), String::from("adiós"));
Aquí vemos que debemos agregar HashMap
, ya que no forma parte del prelude
, aunque sí lo importamos desde la librería estándar de Rust.
La declaración explícita sería let mut spanish: HashMap<String, String>
.
Acceder a los elementos de un hash map en Rust
use std::collections::HashMap;
let mut spanish: HashMap<String, String> = HashMap::new();
spanish.insert(String::from("hi"), String::from("hola"));
spanish.insert(String::from("bye"), String::from("adiós"));
let saludo_en = "hi".to_string();
let saludo_es = spanish.get(&saludo_en).map_or("_NO_VALUE_", String::as_str);
println!("{}", saludo_es);
El método get
regresa un Option<&V>
, si no es un valor válido, regresará None
. En el ejemplo, si saludo_en
no tiene una clave válida como “hi” o “bye” su valor por default será “NO_VALUE“, de lo contrario regresará el valor devuelto como &V
que get
regresó.
Iterar sobre un hash map
use std::collections::HashMap;
let mut spanish: HashMap<String, String> = HashMap::new();
spanish.insert(String::from("hi"), String::from("hola"));
spanish.insert(String::from("bye"), String::from("adiós"));
for (key, value) in &spanish {
println!("{key}: {value}");
}
NOTA: es muy importante saber que al iterar sobre un hash map el orden será arbitrario, o sea, el orden en que están definidos no será obligatoriamente como nos lo mostrará en el ciclo.
Ownership en los hash maps
Para los tipos de datos que implementen el trait Copy
, como el i32
los valores son copiados dentro del hash map. Para los que no, comoString
, sus valores serán “moved” al hash map.
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
Si tratamos de usar o imprimir field_name
o field_value
nos saltará el error del ownership.
Modificar un hash map en Rust
Un hash map, solo debe tener un valor único para cada key
, aunque sus value
puedan ser iguales. Por eso hay que revisar la forma en que estas key
serán gestionadas cuando agreguemos un nuevo valor.
Sobreescribir
Si agregamos un elemento con una key
que ya existe, el valor que ésta tiene será reemplazado:
use std::collections::HashMap;
let mut edades = HashMap::new();
edades.insert(String::from("manuel"), 35);
edades.insert(String::from("manuel"), 38);
println!("{:?}", edades);
Veremos que se imprime {"manuel": 38}
, dejando el último valor asignado a nuestro key
.
Agregando
Por lo general cuando ya tenemos datos en nuestro hash map y queremos agregar un valor, validamos si un key
existe. Si éste existe, dejamos (o sobreescribimos) su valor tal cuál, si no, lo agregamos como un nuevo elemento.
Rust tiene un método llamada entry
, que nos ayuda a gestionar esto. Este método regresa un enum
llamada Entry
que representa un valor que puede o no existir:
use std::collections::HashMap;
let mut edades = HashMap::new();
edades.insert(String::from("manuel"), 30);
edades.entry(String::from("fulano")).or_insert(50);
edades.entry(String::from("manuel")).or_insert(42);
println!("{:?}", edades);
Aquí vemos que al hacer el entry
de nuestra clave “manuel”, nos regresaría el Enum
como un valor existente, entonces no reemplaza el valor enviado 42
. Lo contrario para con “fulano”, ya que el enum regresa un valor no existente, por lo que or_insert
entra en juego y agrega nuestro par de valores: “fulano” y “50”.
Básicamente la diferencia entre insert
y entry
es que la primera reemplaza los valores, mientras que la segunda verifica que exista y tú decides qué hacer.
Veamos otra variante del código para entender mejor que es eso de Entry
:
use std::collections::hash_map::Entry::Occupied;
use std::collections::hash_map::Entry::Vacant;
use std::collections::HashMap;
let mut edades = HashMap::new();
edades.insert(String::from("manuel"), 30);
edades.entry(String::from("fulano")).or_insert(50);
let ed = edades.entry(String::from("manuel"));
match ed {
Occupied(p) => {
println!("Existe: {}", p.get());
}
Vacant(p) => {
println!("No existe: ");
p.insert(42);
}
}
println!("{:?}", edades);
Entry
tiene dos posible valores: Occupied
y Vacant
y son importadas desde std::collections::hash_map::Entry
. Cuando aplicamos el pattern matching realizamos la misma acción que cuando usamos or_insert
. Así que creo que esta última es mucho más sencilla de usar en la mayoría de los casos para ahorrarnos código.
Actualizar un valor basado en su valor anterior
Ya vimos como agregar un valor o reemplazarlo, pero qué pasa cuando queremos actualizar el value
de una key
basado en el valor que tiene, por ejemplo, los goles de un equipo, o actualizar la cantidad de productos en un carrito de compras.
Veamos un ejemplo:
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
Aquí creamos un new Hash Map y hacemos un ciclo de acuerdo a la cantidad de elementos obtenidos al separar una cadena de texto por espacios en blanco. En el ejemplo, vemos que si separamos la cadena hello world wonderful world
por espacios nos da 4 elementos (del 0
al 3
).
A cada elemento le vamos a intentar agregar un cero y basándonos en el resultado (Occupied
o Vacant
) haremos la operación *count += 1
. Aquí or_insert
nos regresa una referencia mutable del valor de la key
, entonces con el asterisco *
leemos el valor y sumamos uno. Por ejemplo, en el primer caso para elemento no existirá, así que insertará hello
en el hash map y enseguida le sumará 1
. Para el caso de world
es lo mismo, pero como lo vuelve a encontrar, la segunda vez, regresa 1
, si a esto le sumamos uno más, el map
nos da como resultado:
{"world": 2, "hello": 1, "wonderful": 1}
Con esto cubrimos los hash maps en Rust. Espero que les sea útil. Más información acá.
Gracias por leer.