Hash es un trait fundamental para la eficiencia de colecciones como HashMap y HashSet. Su función es transformar un valor en un índice numérico (hash). Existe un contrato crítico: si dos objetos son iguales según el trait Eq, sus valores hash deben ser idénticos. Si implementas PartialEq de forma manual, es obligatorio implementar Hash manualmente para garantizar esta consistencia; de lo contrario, el HashMap podría no encontrar un elemento que ya ha sido insertado.
Default proporciona un valor inicial estandarizado para un tipo mediante T::default() o Default::default(). Es extremadamente útil para inicializar estructuras complejas con valores base. Si todos los campos de una struct implementan Default, puedes usar #[derive(Default)] para automatizarlo.
La conversión de tipos en Rust se apoya en From e Into. From<T> se utiliza para implementaciones infalibles (que nunca fallan). La convención dicta que siempre se debe implementar From<T> for U, ya que esto otorga automáticamente la implementación de Into<U> for T. Mientras que From es para definir la lógica de conversión, Into es ideal para definir límites de tipos en funciones genéricas, permitiendo que una función sea flexible al aceptar múltiples tipos que puedan convertirse al tipo requerido. Para conversiones que pueden fallar (como convertir un i32 a un u8), se deben utilizar TryFrom y TryInto, los cuales retornan un Result.
use std::collections::HashSet;
// Implementación de Hash, PartialEq y Eq mediante derive
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Usuario {
id: u32,
nombre: String,
}
// Implementación manual de Default para definir un estado inicial
impl Default for Usuario {
fn default() -> Self {
Self {
id: 0,
nombre: String::from("Anónimo"),
}
}
}
struct Identificador(u32);
// Implementación de From para conversión infalible de u32 a Identificador
impl From<u32> for Identificador {
fn from(valor: u32) -> Self {
Identificador(valor)
}
}
// Función genérica que acepta cualquier tipo que pueda convertirse en String
fn mostrar_nombre<T: Into<String>>(nombre: T) {
let nombre_string: String = nombre.into();
println!("Nombre procesado: {}", nombre_string);
}
fn main() {
// 1. Uso de Hash y Default en colecciones
let mut conjunto = HashSet::new();
let u1 = Usuario { id: 1, nombre: String::from("Alice") };
let u_default = Usuario::default();
conjunto.insert(u1.clone());
conjunto.insert(u_default);
println!("Usuarios en el HashSet: {:?}", conjunto);
// 2. Uso de From e Into
// Conversión explícita usando From
let id_fijo = Identificador::from(42);
println!("ID creado desde u32: {}", id_fijo.0);
// Uso de Into gracias a la flexibilidad de la función genérica
mostrar_nombre("Bob"); // Pasa un &str
mostrar_nombre(String::from("C")); // Pasa un String
}
Explicación del Código
Usuario: Esta estructura utiliza#[derive(Hash, PartialEq, Eq)]. Al usarderive, Rust garantiza que el hash se base en los camposidynombre, cumpliendo el contrato de que siu1 == u2, sus hashes serán iguales.Usuario::default(): ImplementamosDefaultmanualmente para que cada nuevoUsuariono vacío comience conid: 0y el nombre"Anónimo".Identificador::from(valor: u32): ImplementamosFrom<u32>paraIdentificador. Esto permite que cualquier código que espere unIdentificadorpueda recibir unu32y convertirlo automáticamente.mostrar_nombre<T: Into<String>>(nombre: T): Esta función es el ejemplo de flexibilidad. El límite de tipoT: Into<String>permite que la función acepte tanto un&strcomo unString(o cualquier tipo que implemente esa conversión), llamando a.into()para normalizar el dato a unString.conjunto.insert(...): ElHashSetutiliza internamente el traitHashdeUsuariopara posicionar los objetos en la memoria de forma eficiente.
N° 49