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ícateDebug 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{:#?}
paraDebug
,{}
paraDisplay
. - Requisitos: Muchas funciones y macros de Rust requieren
Debug
(comoassert_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:
- Puede implementarse para tipos que contienen recursos que necesitan duplicación especial
- Permite controlar exactamente cuándo ocurre la duplicación
- 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 deClone
, por lo que todo tipo que implementaCopy
debe implementarClone
.
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:
- Reflexividad: un valor siempre es igual a sí mismo (
a == a
) - Simetría: si
a == b
entoncesb == a
- Transitividad: si
a == b
yb == c
, entoncesa == 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:
Eq
es un subtrait dePartialEq
- Todo tipo que implementa
Eq
debe implementarPartialEq
primero 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:
- Comparar primero campos pequeños o que tienen más probabilidad de ser diferentes
- Usar comprobaciones rápidas antes de comparaciones costosas
- 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.
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.
Introducción A Rust
Introducción Y Entorno
Primer Programa
Introducción Y Entorno
Instalación Del Entorno
Introducción Y Entorno
Funciones
Sintaxis
Operadores
Sintaxis
Estructuras De Control Condicional
Sintaxis
Arrays Y Strings
Sintaxis
Manejo De Errores Panic
Sintaxis
Variables Y Tipos Básicos
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Colecciones Estándar
Estructuras De Datos
Option Y Result
Estructuras De Datos
Pattern Matching
Estructuras De Datos
Estructuras (Structs)
Estructuras De Datos
Enumeraciones Enums
Estructuras De Datos
El Concepto De Ownership
Ownership
Lifetimes Básicos
Ownership
Slices Y Referencias Parciales
Ownership
References Y Borrowing
Ownership
Funciones Anónimas Closures
Abstracción
Traits De La Biblioteca Estándar
Abstracción
Traits
Abstracción
Generics
Abstracción
Channels Y Paso De Mensajes
Concurrencia
Memoria Compartida Segura
Concurrencia
Threads Y Sincronización Básica
Concurrencia
Introducción A Tokio
Asincronía
Fundamentos Asíncronos Y Futures
Asincronía
Async/await
Asincronía
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.