Rust

Rust

Tutorial Rust: Traits de la biblioteca estándar

Aprende los traits Debug, Display, Clone, Copy, PartialEq y Eq en Rust para gestionar representaciones, clonación y comparaciones de datos eficazmente.

Aprende Rust y certifícate

Debug y Display

Los traits Debug y Display son fundamentales en Rust para controlar cómo se representan los valores como texto. Ambos permiten convertir tipos de datos en cadenas de texto, pero con propósitos y comportamientos diferentes.

Debug: representación para desarrolladores

El trait Debug está diseñado principalmente para depuración y desarrollo. Proporciona una representación textual que, aunque no siempre es elegante, contiene toda la información necesaria para entender el estado interno de un valor.

La forma más sencilla de implementar Debug es mediante la derivación automática:

#[derive(Debug)]
struct Punto {
    x: f64,
    y: f64,
}

fn main() {
    let punto = Punto { x: 3.0, y: 4.0 };
    
    // Usando el formato de depuración con {:?}
    println!("Punto: {:?}", punto);
    
    // Formato pretty-print con {:#?} (más legible)
    println!("Punto (formato bonito): {:#?}", punto);
}

La salida sería:

Punto: Punto { x: 3.0, y: 4.0 }
Punto (formato bonito): 
Punto {
    x: 3.0,
    y: 4.0,
}

Observa que usamos {:?} para el formato de depuración básico y {:#?} para una versión con mejor formato visual. Estos especificadores de formato son exclusivos para tipos que implementan Debug.

También podemos implementar Debug manualmente cuando necesitamos un control más preciso:

use std::fmt;

struct Coordenada {
    x: f64,
    y: f64,
    sistema: String,
}

impl fmt::Debug for Coordenada {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Coordenada")
            .field("x", &self.x)
            .field("y", &self.y)
            .field("sistema", &self.sistema)
            .finish()
    }
}

fn main() {
    let coord = Coordenada { 
        x: 10.5, 
        y: 20.3, 
        sistema: String::from("cartesiano") 
    };
    
    println!("Coordenada: {:?}", coord);
}

La implementación manual nos da flexibilidad para personalizar la representación, especialmente útil para tipos complejos o cuando queremos ocultar ciertos campos.

Display: representación para usuarios

Mientras que Debug está orientado a desarrolladores, el trait Display está diseñado para crear representaciones destinadas a usuarios finales. A diferencia de Debug, Display no puede derivarse automáticamente, ya que Rust no puede asumir cómo quieres mostrar tu tipo a los usuarios.

Para implementar Display, debemos hacerlo manualmente:

use std::fmt;

struct Temperatura {
    valor: f64,
    escala: char,
}

impl fmt::Display for Temperatura {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:.1}°{}", self.valor, self.escala)
    }
}

fn main() {
    let temp = Temperatura { valor: 36.6, escala: 'C' };
    
    // Usando el formato de visualización con {}
    println!("La temperatura es: {}", temp);
}

La salida sería:

La temperatura es: 36.6°C

Observa que usamos el especificador {} para tipos que implementan Display, a diferencia de {:?} para Debug.

Diferencias clave y usos prácticos

Estas son las principales diferencias entre ambos traits:

  • Propósito: Debug para desarrolladores y depuración, Display para usuarios finales.
  • Derivación: Debug puede derivarse automáticamente, Display requiere implementación manual.
  • Especificadores: {:?} y {:#?} para Debug, {} para Display.
  • Requisitos: Muchas funciones y macros de Rust requieren Debug (como assert_eq!).

Un caso de uso común es implementar ambos traits para un mismo tipo:

use std::fmt;

#[derive(Debug)]
struct Producto {
    nombre: String,
    precio: f64,
    stock: u32,
}

impl fmt::Display for Producto {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} - ${:.2}", self.nombre, self.precio)
    }
}

fn main() {
    let producto = Producto {
        nombre: String::from("Teclado mecánico"),
        precio: 89.99,
        stock: 15,
    };
    
    // Para usuarios (solo muestra nombre y precio)
    println!("Producto: {}", producto);
    
    // Para depuración (muestra todos los campos)
    println!("Información completa: {:?}", producto);
}

La salida sería:

Producto: Teclado mecánico - $89.99
Información completa: Producto { nombre: "Teclado mecánico", precio: 89.99, stock: 15 }

Debug y Display en la práctica

Estos traits son esenciales en situaciones como:

  • Logging: Debug es perfecto para registrar el estado interno de objetos durante la depuración.
#[derive(Debug)]
struct Usuario {
    id: u64,
    nombre: String,
    activo: bool,
}

fn procesar_usuario(usuario: &Usuario) {
    // Registramos el estado completo para depuración
    println!("Procesando usuario: {:?}", usuario);
    
    // Resto de la lógica...
}
  • Interfaz de usuario: Display es ideal para mostrar información al usuario final.
struct Moneda {
    cantidad: f64,
    divisa: String,
}

impl fmt::Display for Moneda {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        // Formato específico para cada divisa
        match self.divisa.as_str() {
            "EUR" => write!(f, "{}€", self.cantidad),
            "USD" => write!(f, "${}", self.cantidad),
            _ => write!(f, "{} {}", self.cantidad, self.divisa),
        }
    }
}
  • Mensajes de error: Combinando ambos para proporcionar mensajes de error útiles.
#[derive(Debug)]
enum ErrorOperacion {
    DivisionPorCero,
    DesbordamientoNumerico,
    OperacionInvalida,
}

impl fmt::Display for ErrorOperacion {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ErrorOperacion::DivisionPorCero => 
                write!(f, "Error: División por cero"),
            ErrorOperacion::DesbordamientoNumerico => 
                write!(f, "Error: Desbordamiento numérico"),
            ErrorOperacion::OperacionInvalida => 
                write!(f, "Error: Operación no válida"),
        }
    }
}

Formateando colecciones

Para tipos que contienen colecciones, podemos implementar Debug o Display de forma que muestre los elementos de manera personalizada:

use std::fmt;

struct Carrito {
    items: Vec<String>,
}

impl fmt::Display for Carrito {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.items.is_empty() {
            return write!(f, "Carrito vacío");
        }
        
        writeln!(f, "Carrito con {} productos:", self.items.len())?;
        for (i, item) in self.items.iter().enumerate() {
            writeln!(f, "  {}. {}", i + 1, item)?;
        }
        Ok(())
    }
}

fn main() {
    let carrito = Carrito {
        items: vec![
            String::from("Laptop"),
            String::from("Ratón"),
            String::from("Teclado"),
        ],
    };
    
    println!("{}", carrito);
}

La salida sería:

Carrito con 3 productos:
  1. Laptop
  2. Ratón
  3. Teclado

Uso con Result y Option

Los traits Debug y Display son especialmente útiles cuando trabajamos con tipos como Result y Option:

fn dividir(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("División por cero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    let resultado = dividir(10.0, 2.0);
    
    // Usando Debug para mostrar la estructura completa
    println!("Resultado (Debug): {:?}", resultado);
    
    // Usando Display (implementado automáticamente para Result)
    println!("Resultado (Display): {}", resultado.unwrap());
    
    // Con Option
    let valor_opcional: Option<i32> = Some(42);
    println!("Valor opcional: {:?}", valor_opcional);
}

La biblioteca estándar ya implementa Display para Result y Option cuando sus tipos genéricos también implementan Display, lo que facilita su uso en mensajes de error y salidas de usuario.

Clone y Copy

En Rust, la gestión de memoria es un aspecto fundamental que afecta directamente a cómo se transfieren los valores entre variables. Los traits Clone y Copy son mecanismos esenciales que determinan cómo se duplican los datos en memoria.

Semántica de movimiento por defecto

Antes de profundizar en estos traits, es importante entender que Rust utiliza por defecto una semántica de movimiento (move semantics). Cuando asignas un valor a otra variable o lo pasas como argumento, el valor se mueve y la variable original ya no es válida:

fn main() {
    let s1 = String::from("hola");
    let s2 = s1;  // s1 se mueve a s2
    
    // println!("{}", s1);  // Error: valor movido
    println!("{}", s2);  // Funciona correctamente
}

Esta característica previene problemas como el doble liberado (double free) y es parte del sistema de propiedad (ownership) de Rust.

El trait Copy

El trait Copy permite que un tipo sea duplicado bit a bit cuando se asigna o se pasa como argumento. Los tipos que implementan Copy no se mueven, sino que se copian automáticamente:

fn main() {
    let x = 5;  // Los enteros implementan Copy
    let y = x;  // x se copia a y
    
    println!("x: {}, y: {}", x, y);  // Ambos son válidos
}

Para que un tipo pueda implementar Copy, debe cumplir una regla fundamental: todos sus componentes también deben implementar Copy. Esto significa que no puede contener tipos que requieran recursos especiales para duplicarse, como String o Vec.

Podemos derivar Copy automáticamente para nuestros tipos:

#[derive(Copy, Clone)]  // Copy requiere Clone
struct Punto {
    x: f64,
    y: f64,
}

fn main() {
    let p1 = Punto { x: 1.0, y: 2.0 };
    let p2 = p1;  // p1 se copia a p2
    
    println!("p1: ({}, {})", p1.x, p1.y);  // Sigue siendo válido
    println!("p2: ({}, {})", p2.x, p2.y);
}

Observa que para implementar Copy, también debemos implementar Clone, ya que Copy es un subtrait de Clone.

El trait Clone

Mientras que Copy proporciona duplicación implícita y automática, Clone ofrece duplicación explícita y controlada a través del método clone():

fn main() {
    let s1 = String::from("hola");
    let s2 = s1.clone();  // Creamos una copia profunda
    
    println!("s1: {}", s1);  // Ambas variables son válidas
    println!("s2: {}", s2);
}

Clone es más flexible que Copy porque:

  1. Puede implementarse para tipos que contienen recursos que necesitan duplicación especial
  2. Permite controlar exactamente cuándo ocurre la duplicación
  3. Puede personalizarse para implementar lógica específica durante la clonación

Podemos derivar Clone automáticamente si todos los campos del tipo implementan Clone:

#[derive(Clone)]
struct Usuario {
    nombre: String,
    edad: u32,
    activo: bool,
}

fn main() {
    let usuario1 = Usuario {
        nombre: String::from("Ana"),
        edad: 28,
        activo: true,
    };
    
    let usuario2 = usuario1.clone();
    
    println!("Usuario clonado: {} ({} años)", usuario2.nombre, usuario2.edad);
}

Implementación manual de Clone

Aunque la derivación automática es conveniente, a veces necesitamos personalizar el comportamiento de clonación:

use std::clone::Clone;

struct CacheRecursos {
    datos: Vec<String>,
    contador_usos: usize,
}

impl Clone for CacheRecursos {
    fn clone(&self) -> Self {
        println!("Clonando caché de recursos...");
        
        // Podemos personalizar la clonación
        CacheRecursos {
            datos: self.datos.clone(),
            contador_usos: 0,  // Reiniciamos el contador en la copia
        }
    }
}

fn main() {
    let cache1 = CacheRecursos {
        datos: vec![String::from("recurso1"), String::from("recurso2")],
        contador_usos: 10,
    };
    
    let cache2 = cache1.clone();
    
    println!("Longitud de datos en cache2: {}", cache2.datos.len());
    println!("Contador de usos en cache2: {}", cache2.contador_usos);
}

En este ejemplo, personalizamos la clonación para reiniciar el contador de usos en la copia, mientras que los datos se clonan normalmente.

Diferencias clave entre Copy y Clone

Es importante entender las diferencias fundamentales entre estos traits:

  • Comportamiento: Copy es implícito y automático, Clone es explícito y requiere llamar a .clone().
  • Restricciones: Copy tiene restricciones más estrictas (solo tipos que pueden copiarse bit a bit).
  • Rendimiento: Copy generalmente es más eficiente porque es una simple copia de bits.
  • Jerarquía: Copy es un subtrait de Clone, por lo que todo tipo que implementa Copy debe implementar Clone.

Esta tabla resume los tipos comunes y su implementación de estos traits:

Tipo Copy Clone
Tipos primitivos (i32, f64, bool)
Referencias inmutables (&T)
Referencias mutables (&mut T)
String, Vec, Box, etc.
Tuplas y arrays (si sus elementos son Copy)

Casos de uso prácticos

Clonación selectiva de campos

A veces queremos clonar solo ciertos campos de una estructura:

#[derive(Debug)]
struct Documento {
    contenido: String,
    id: u64,
    timestamp: u64,
}

impl Clone for Documento {
    fn clone(&self) -> Self {
        // Generamos un nuevo ID y timestamp para la copia
        let nuevo_id = self.id + 1;
        let nuevo_timestamp = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs();
        
        Documento {
            contenido: self.contenido.clone(),
            id: nuevo_id,
            timestamp: nuevo_timestamp,
        }
    }
}

fn main() {
    let doc1 = Documento {
        contenido: String::from("Contenido importante"),
        id: 1000,
        timestamp: 1634567890,
    };
    
    let doc2 = doc1.clone();
    
    println!("Documento original: {:?}", doc1);
    println!("Documento clonado: {:?}", doc2);
}

Optimización con Clone-on-Write

Podemos implementar un patrón de "clonar al escribir" para optimizar el rendimiento:

use std::rc::Rc;

struct Texto {
    contenido: Rc<String>,
    es_modificado: bool,
}

impl Texto {
    fn new(s: &str) -> Self {
        Texto {
            contenido: Rc::new(String::from(s)),
            es_modificado: false,
        }
    }
    
    fn modificar(&mut self, nuevo_texto: &str) {
        // Solo clonamos si no somos el único propietario
        if Rc::strong_count(&self.contenido) > 1 {
            self.contenido = Rc::new(String::from(nuevo_texto));
        } else {
            // Podemos modificar directamente
            unsafe {
                Rc::get_mut_unchecked(&mut self.contenido).clear();
                Rc::get_mut_unchecked(&mut self.contenido).push_str(nuevo_texto);
            }
        }
        self.es_modificado = true;
    }
}

impl Clone for Texto {
    fn clone(&self) -> Self {
        Texto {
            contenido: Rc::clone(&self.contenido),  // Solo clona la referencia
            es_modificado: false,
        }
    }
}

fn main() {
    let mut texto1 = Texto::new("Documento original");
    let texto2 = texto1.clone();  // No clona el contenido, solo la referencia
    
    println!("Referencias al contenido: {}", Rc::strong_count(&texto1.contenido));
    
    texto1.modificar("Documento modificado");  // Ahora sí clona el contenido
    
    println!("Referencias después de modificar: {}", Rc::strong_count(&texto1.contenido));
}

Este patrón es útil cuando trabajamos con grandes estructuras de datos que raramente se modifican.

Copy y Clone en funciones

El uso de estos traits afecta significativamente cómo escribimos funciones:

// Para tipos Copy, podemos pasar por valor sin perder el original
fn calcular_distancia(punto: Punto) -> f64 {
    (punto.x.powi(2) + punto.y.powi(2)).sqrt()
}

// Para tipos no-Copy, tenemos varias opciones:
// 1. Pasar por referencia (no consume)
fn longitud(s: &String) -> usize {
    s.len()
}

// 2. Tomar propiedad (consume)
fn procesar_y_consumir(s: String) {
    println!("Procesando: {}", s);
    // s se destruye al final de la función
}

// 3. Clonar explícitamente (si necesitamos una copia)
fn procesar_y_devolver(s: String) -> (String, usize) {
    let len = s.len();
    (s, len)  // Devolvemos el original
}

fn main() {
    let p = Punto { x: 3.0, y: 4.0 };
    println!("Distancia: {}", calcular_distancia(p));
    println!("Punto original: ({}, {})", p.x, p.y);  // p sigue siendo válido
    
    let mensaje = String::from("Hola mundo");
    println!("Longitud: {}", longitud(&mensaje));  // Préstamo, no consume
    
    let (msg, len) = procesar_y_devolver(mensaje);  // mensaje se mueve
    println!("Mensaje: {} (longitud: {})", msg, len);
}

Consideraciones de rendimiento

Es importante entender las implicaciones de rendimiento:

  • Copy es generalmente más eficiente porque es una simple copia bit a bit.
  • Clone puede ser costoso para estructuras grandes o complejas.
  • Clonar innecesariamente puede afectar el rendimiento.
fn main() {
    // Eficiente: los enteros son Copy
    let nums1: Vec<i32> = (0..1000).collect();
    let nums2 = nums1;  // Mueve el Vec, no copia los elementos
    
    // Potencialmente costoso: clona todo el vector y sus elementos
    let palabras1: Vec<String> = vec![String::from("rust"); 1000];
    let palabras2 = palabras1.clone();
    
    // Más eficiente: usa referencias cuando no necesitas propiedad
    fn procesar_vector(v: &[String]) {
        println!("Vector con {} elementos", v.len());
    }
    
    procesar_vector(&palabras2);  // Préstamo, no clonación
}

Al diseñar APIs en Rust, es una buena práctica considerar cuidadosamente cuándo usar Clone y cuándo usar referencias para evitar copias innecesarias.

PartialEq y Eq

La comparación de valores es una operación fundamental en cualquier lenguaje de programación. En Rust, esta funcionalidad se implementa a través de los traits PartialEq y Eq, que permiten determinar si dos valores son iguales o diferentes.

Comparaciones con PartialEq

El trait PartialEq define la operación de igualdad parcial entre valores. Es el responsable de implementar los operadores == y != en Rust:

pub trait PartialEq<Rhs = Self> {
    fn eq(&self, other: &Rhs) -> bool;
    
    fn ne(&self, other: &Rhs) -> bool {
        !self.eq(other)
    }
}

Observa que solo necesitas implementar el método eq(), ya que ne() tiene una implementación predeterminada que simplemente niega el resultado de eq().

La forma más sencilla de obtener esta funcionalidad es mediante la derivación automática:

#[derive(PartialEq)]
struct Libro {
    titulo: String,
    autor: String,
    año: u32,
}

fn main() {
    let libro1 = Libro {
        titulo: String::from("El Quijote"),
        autor: String::from("Miguel de Cervantes"),
        año: 1605,
    };
    
    let libro2 = Libro {
        titulo: String::from("El Quijote"),
        autor: String::from("Miguel de Cervantes"),
        año: 1605,
    };
    
    let libro3 = Libro {
        titulo: String::from("Cien años de soledad"),
        autor: String::from("Gabriel García Márquez"),
        año: 1967,
    };
    
    println!("¿libro1 == libro2? {}", libro1 == libro2); // true
    println!("¿libro1 == libro3? {}", libro1 == libro3); // false
}

Cuando derivamos PartialEq, Rust compara cada campo de la estructura utilizando su propia implementación de PartialEq. Dos instancias son iguales solo si todos sus campos son iguales.

Implementación manual de PartialEq

A veces necesitamos personalizar la lógica de comparación. Por ejemplo, podríamos querer comparar libros solo por su título y autor, ignorando el año de publicación:

struct Libro {
    titulo: String,
    autor: String,
    año: u32,
}

impl PartialEq for Libro {
    fn eq(&self, other: &Self) -> bool {
        self.titulo == other.titulo && self.autor == other.autor
        // Ignoramos el campo 'año' intencionadamente
    }
}

fn main() {
    let primera_edicion = Libro {
        titulo: String::from("El Quijote"),
        autor: String::from("Miguel de Cervantes"),
        año: 1605,
    };
    
    let edicion_moderna = Libro {
        titulo: String::from("El Quijote"),
        autor: String::from("Miguel de Cervantes"),
        año: 2010,
    };
    
    // Ahora son iguales porque solo comparamos título y autor
    println!("¿Mismo libro? {}", primera_edicion == edicion_moderna); // true
}

También podemos implementar PartialEq para comparar tipos diferentes:

struct ISBN {
    codigo: String,
}

impl PartialEq<ISBN> for Libro {
    fn eq(&self, isbn: &ISBN) -> bool {
        // Lógica para verificar si el libro corresponde al ISBN
        self.titulo == "El Quijote" && isbn.codigo == "978-84-376-0494-7"
    }
}

fn main() {
    let libro = Libro {
        titulo: String::from("El Quijote"),
        autor: String::from("Miguel de Cervantes"),
        año: 1605,
    };
    
    let isbn = ISBN {
        codigo: String::from("978-84-376-0494-7"),
    };
    
    println!("¿El libro corresponde al ISBN? {}", libro == isbn);
}

El trait Eq: igualdad total

El trait Eq extiende PartialEq para indicar que la relación de igualdad es una relación de equivalencia, lo que significa que cumple tres propiedades:

  1. Reflexividad: un valor siempre es igual a sí mismo (a == a)
  2. Simetría: si a == b entonces b == a
  3. Transitividad: si a == b y b == c, entonces a == c

A diferencia de PartialEq, el trait Eq no tiene métodos propios, simplemente actúa como un marcador que garantiza estas propiedades:

pub trait Eq: PartialEq<Self> {}

La principal diferencia entre PartialEq y Eq es que PartialEq permite relaciones de igualdad que no son totales. El ejemplo clásico es el tipo f32 (números de punto flotante), que implementa PartialEq pero no Eq debido a la existencia del valor NaN (Not a Number):

fn main() {
    let x = f32::NAN;
    
    // NaN no es igual a nada, ni siquiera a sí mismo
    println!("¿NaN == NaN? {}", x == x); // false
}

Este comportamiento viola la propiedad de reflexividad, por lo que f32 y f64 no pueden implementar Eq.

Para nuestros propios tipos, podemos derivar Eq si todos sus campos también implementan Eq:

#[derive(PartialEq, Eq)]
struct Usuario {
    id: u64,
    nombre: String,
    activo: bool,
}

Implementación manual de Eq

La implementación manual de Eq es sencilla, ya que no requiere métodos adicionales:

struct Coordenada {
    x: i32,
    y: i32,
}

impl PartialEq for Coordenada {
    fn eq(&self, other: &Self) -> bool {
        self.x == other.x && self.y == other.y
    }
}

// Implementación vacía, solo indica que cumple con las propiedades de Eq
impl Eq for Coordenada {}

Relación entre PartialEq y Eq

Es importante entender la jerarquía entre estos traits:

  1. Eq es un subtrait de PartialEq
  2. Todo tipo que implementa Eq debe implementar PartialEq primero
  3. Eq no añade nuevos métodos, solo garantiza propiedades adicionales

Esta tabla muestra algunos tipos comunes y su implementación de estos traits:

Tipo PartialEq Eq
Enteros (i32, u64, etc.)
Flotantes (f32, f64)
bool
char
String
Option (si T implementa los traits)
Result<T, E> (si T y E implementan los traits)

Casos de uso prácticos

Uso en colecciones

Los traits PartialEq y Eq son fundamentales para trabajar con colecciones:

use std::collections::HashSet;

#[derive(Debug, PartialEq, Eq, Hash)]
struct Producto {
    id: u32,
    nombre: String,
}

fn main() {
    let mut productos = HashSet::new();
    
    productos.insert(Producto { id: 1, nombre: String::from("Teclado") });
    productos.insert(Producto { id: 2, nombre: String::from("Ratón") });
    productos.insert(Producto { id: 1, nombre: String::from("Teclado") }); // Duplicado
    
    // HashSet usa Eq para eliminar duplicados
    println!("Número de productos únicos: {}", productos.len()); // 2
    
    for producto in &productos {
        println!("{:?}", producto);
    }
}

Observa que para usar HashSet, necesitamos implementar también el trait Hash, que está estrechamente relacionado con Eq. La regla general es: si dos valores son iguales según Eq, deben tener el mismo hash.

Búsqueda y filtrado

Estos traits permiten buscar elementos en colecciones:

#[derive(PartialEq)]
struct Empleado {
    id: u32,
    nombre: String,
    departamento: String,
}

fn main() {
    let empleados = vec![
        Empleado { id: 1, nombre: String::from("Ana"), departamento: String::from("Ventas") },
        Empleado { id: 2, nombre: String::from("Carlos"), departamento: String::from("IT") },
        Empleado { id: 3, nombre: String::from("Elena"), departamento: String::from("Ventas") },
    ];
    
    // Buscar un empleado específico
    let buscar = Empleado { 
        id: 2, 
        nombre: String::from("Carlos"), 
        departamento: String::from("IT") 
    };
    
    if let Some(posicion) = empleados.iter().position(|e| e == &buscar) {
        println!("Empleado encontrado en la posición {}", posicion);
    }
    
    // Filtrar empleados por departamento
    let departamento_objetivo = String::from("Ventas");
    let ventas: Vec<_> = empleados.iter()
        .filter(|e| e.departamento == departamento_objetivo)
        .collect();
    
    println!("Empleados en Ventas: {}", ventas.len());
}

Comparaciones personalizadas para ordenación

Aunque PartialEq y Eq se usan para igualdad, a menudo se implementan junto con PartialOrd y Ord para permitir ordenación:

#[derive(Debug, PartialEq, Eq)]
struct Versión {
    mayor: u32,
    menor: u32,
    parche: u32,
}

impl PartialOrd for Versión {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Versión {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        // Comparamos primero por versión mayor, luego menor, luego parche
        match self.mayor.cmp(&other.mayor) {
            std::cmp::Ordering::Equal => {
                match self.menor.cmp(&other.menor) {
                    std::cmp::Ordering::Equal => self.parche.cmp(&other.parche),
                    ordering => ordering,
                }
            },
            ordering => ordering,
        }
    }
}

fn main() {
    let versiones = vec![
        Versión { mayor: 1, menor: 2, parche: 3 },
        Versión { mayor: 2, menor: 0, parche: 0 },
        Versión { mayor: 1, menor: 10, parche: 0 },
        Versión { mayor: 1, menor: 2, parche: 5 },
    ];
    
    let mut ordenadas = versiones.clone();
    ordenadas.sort();
    
    println!("Versiones ordenadas:");
    for v in ordenadas {
        println!("{}.{}.{}", v.mayor, v.menor, v.parche);
    }
}

Implementación para enumeraciones

Las enumeraciones también pueden implementar estos traits:

#[derive(Debug, PartialEq, Eq)]
enum Estado {
    Pendiente,
    EnProceso { progreso: u8 },
    Completado,
    Error(String),
}

fn main() {
    let tarea1 = Estado::EnProceso { progreso: 75 };
    let tarea2 = Estado::EnProceso { progreso: 75 };
    let tarea3 = Estado::Completado;
    
    println!("¿tarea1 == tarea2? {}", tarea1 == tarea2); // true
    println!("¿tarea1 == tarea3? {}", tarea1 == tarea3); // false
    
    // También funciona con variantes que contienen datos
    let error1 = Estado::Error(String::from("Conexión perdida"));
    let error2 = Estado::Error(String::from("Conexión perdida"));
    
    println!("¿error1 == error2? {}", error1 == error2); // true
}

Comparaciones parciales personalizadas

A veces queremos implementar comparaciones que solo consideran ciertos aspectos:

struct Documento {
    id: u64,
    título: String,
    contenido: String,
    metadatos: Vec<String>,
}

// Implementación estándar que compara todos los campos
impl PartialEq for Documento {
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id && 
        self.título == other.título && 
        self.contenido == other.contenido && 
        self.metadatos == other.metadatos
    }
}

// Implementación adicional para comparar solo por ID
struct PorId(u64);

impl PartialEq<PorId> for Documento {
    fn eq(&self, id: &PorId) -> bool {
        self.id == id.0
    }
}

fn main() {
    let doc = Documento {
        id: 12345,
        título: String::from("Informe anual"),
        contenido: String::from("Contenido del informe..."),
        metadatos: vec![String::from("confidencial")],
    };
    
    // Comparar usando solo el ID
    println!("¿Documento con ID 12345? {}", doc == PorId(12345)); // true
    println!("¿Documento con ID 54321? {}", doc == PorId(54321)); // false
}

Consideraciones de rendimiento

La implementación de PartialEq y Eq puede afectar el rendimiento, especialmente para tipos grandes:

#[derive(PartialEq, Eq)]
struct DatosGrandes {
    // Imagina una estructura con muchos campos o colecciones grandes
    valores: Vec<i32>,
    texto: String,
    // ... más campos
}

impl PartialEq for DatosGrandes {
    fn eq(&self, other: &Self) -> bool {
        // Optimización: comparar primero campos pequeños o discriminantes
        if self.texto.len() != other.texto.len() {
            return false;
        }
        
        // Solo si pasan las comprobaciones rápidas, comparamos lo costoso
        self.texto == other.texto && self.valores == other.valores
    }
}

Para estructuras complejas, es recomendable implementar manualmente PartialEq con optimizaciones como:

  1. Comparar primero campos pequeños o que tienen más probabilidad de ser diferentes
  2. Usar comprobaciones rápidas antes de comparaciones costosas
  3. Considerar comparar solo un subconjunto de campos si es semánticamente apropiado

Estos traits son fundamentales en el ecosistema de Rust y su correcta implementación es crucial para crear APIs intuitivas y eficientes.

Aprende Rust online

Otras lecciones de Rust

Accede a todas las lecciones de Rust y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Rust y certifícate

Ejercicios de programación de Rust

Evalúa tus conocimientos de esta lección Traits de la biblioteca estándar con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la diferencia entre los traits Debug y Display para representar valores como texto.
  • Aprender a implementar y derivar Debug y Display en tipos personalizados.
  • Entender la semántica de movimiento en Rust y cómo los traits Clone y Copy afectan la duplicación de datos.
  • Saber cuándo y cómo implementar Clone y Copy, y las diferencias clave entre ellos.
  • Conocer los traits PartialEq y Eq para comparar valores, su implementación automática y manual, y su importancia en colecciones y lógica de programas.