Hash Maps en Rust – Aprende Rust 13/x

hash maps en rust

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.

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.


Posted

in

, , ,

by