Structs: Datos Estructurados en Rust
Las structs nos permiten crear tipos de datos personalizados agrupando valores relacionados. Son fundamentales para organizar y encapsular datos en Rust, similar a las clases en otros lenguajes pero sin herencia.
Definiendo Structs
Struct Básica
// Definir una struct
struct Usuario {
nombre : String ,
email : String ,
edad : u32 ,
activo : bool ,
}
fn main () {
// Crear una instancia
let usuario1 = Usuario {
email : String :: from ( "alguien@ejemplo.com" ),
nombre : String :: from ( "Juan Pérez" ),
activo : true ,
edad : 25 ,
};
// Acceder a los campos
println! ( "Nombre: {}" , usuario1 . nombre);
println! ( "Email: {}" , usuario1 . email);
println! ( "Edad: {}" , usuario1 . edad);
println! ( "¿Activo? {}" , usuario1 . activo);
}
Structs Mutables
struct Contador {
valor : i32 ,
nombre : String ,
}
fn main () {
let mut contador = Contador {
valor : 0 ,
nombre : String :: from ( "Mi Contador" ),
};
println! ( "Valor inicial: {}" , contador . valor);
// Modificar campos (struct debe ser mutable)
contador . valor += 1 ;
contador . nombre = String :: from ( "Contador Actualizado" );
println! ( "Valor actualizado: {}" , contador . valor);
println! ( "Nombre actualizado: {}" , contador . nombre);
}
Sintaxis de Construcción
Field Init Shorthand
fn construir_usuario (email : String , nombre : String ) -> Usuario {
Usuario {
email, // Equivale a email: email
nombre, // Equivale a nombre: nombre
activo : true ,
edad : 18 ,
}
}
fn main () {
let usuario = construir_usuario (
String :: from ( "maria@ejemplo.com" ),
String :: from ( "María García" )
);
println! ( "Usuario creado: {}" , usuario . nombre);
}
Struct Update Syntax
fn main () {
let usuario1 = Usuario {
email : String :: from ( "original@ejemplo.com" ),
nombre : String :: from ( "Usuario Original" ),
activo : true ,
edad : 30 ,
};
// Crear nuevo usuario basado en el anterior
let usuario2 = Usuario {
email : String :: from ( "nuevo@ejemplo.com" ),
nombre : String :: from ( "Usuario Nuevo" ),
.. usuario1 // Toma los valores restantes de usuario1
};
println! ( "Usuario 2 - Edad: {}" , usuario2 . edad); // 30
println! ( "Usuario 2 - Activo: {}" , usuario2 . activo); // true
// Nota: usuario1 ya no es válido si algún campo movido no implementa Copy
// println!("{}", usuario1.nombre); // ERROR si String no implementa Copy
}
Tipos de Structs
Tuple Structs
Structs que se comportan como tuplas con nombres:
// Tuple structs
struct Color ( i32 , i32 , i32 );
struct Punto ( i32 , i32 , i32 );
fn main () {
let negro = Color ( 0 , 0 , 0 );
let origen = Punto ( 0 , 0 , 0 );
// Acceso por índice como tuplas
println! ( "Rojo: {}" , negro . 0 );
println! ( "Verde: {}" , negro . 1 );
println! ( "Azul: {}" , negro . 2 );
// Destructuring
let Color (r, g, b) = negro;
println! ( "RGB: ({r}, {g}, {b})" );
// Los tipos son diferentes aunque tengan la misma estructura
// let error: Color = origen; // ERROR: expected Color, found Punto
}
Unit Structs
Structs sin campos, útiles para traits:
struct AlwaysEqual ;
fn main () {
let subject = AlwaysEqual ;
let another = AlwaysEqual ;
// Útiles para implementar traits sin datos
println! ( "Unit structs creadas" );
}
Implementando Métodos
Métodos Básicos
#[derive( Debug )]
struct Rectangulo {
ancho : u32 ,
alto : u32 ,
}
impl Rectangulo {
// Método que toma &self (referencia inmutable)
fn area ( & self ) -> u32 {
self . ancho * self . alto
}
// Método que toma &mut self (referencia mutable)
fn duplicar_tamañ o ( &mut self ) {
self . ancho *= 2 ;
self . alto *= 2 ;
}
// Método que toma self (toma ownership)
fn destruir ( self ) -> String {
format! ( "Rectángulo {}x{} destruido" , self . ancho, self . alto)
}
// Método con parámetros adicionales
fn puede_contener ( & self , otro : & Rectangulo ) -> bool {
self . ancho >= otro . ancho && self . alto >= otro . alto
}
}
fn main () {
let mut rect1 = Rectangulo {
ancho : 30 ,
alto : 50 ,
};
println! ( "Área: {}" , rect1 . area ());
rect1 . duplicar_tamañ o ();
println! ( "Después de duplicar: {:?}" , rect1);
let rect2 = Rectangulo {
ancho : 10 ,
alto : 40 ,
};
println! ( "¿rect1 puede contener rect2? {}" , rect1 . puede_contener ( & rect2));
// Método que consume la instancia
let mensaje = rect1 . destruir ();
println! ( "{mensaje}" );
// rect1 ya no es válido después de destruir()
}
Funciones Asociadas (Associated Functions)
impl Rectangulo {
// Función asociada (como método estático)
fn nuevo (ancho : u32 , alto : u32 ) -> Rectangulo {
Rectangulo { ancho, alto }
}
// Crear un cuadrado
fn cuadrado (tamaño : u32 ) -> Rectangulo {
Rectangulo {
ancho : tamaño,
alto : tamaño,
}
}
// Constantes asociadas
const RECTANGULO_UNITARIO : Rectangulo = Rectangulo { ancho : 1 , alto : 1 };
}
fn main () {
// Llamar funciones asociadas con ::
let rect1 = Rectangulo :: nuevo ( 10 , 20 );
let cuadrado = Rectangulo :: cuadrado ( 5 );
let unitario = Rectangulo :: RECTANGULO_UNITARIO ;
println! ( "Rectángulo: {:?}" , rect1);
println! ( "Cuadrado: {:?}" , cuadrado);
println! ( "Unitario: {:?}" , unitario);
}
Múltiples Bloques impl
Puedes tener múltiples bloques impl para la misma struct:
struct Persona {
nombre : String ,
edad : u32 ,
}
impl Persona {
fn nueva (nombre : String , edad : u32 ) -> Persona {
Persona { nombre, edad }
}
fn saludar ( & self ) {
println! ( "¡Hola! Soy {}" , self . nombre);
}
}
// Segundo bloque impl para la misma struct
impl Persona {
fn cumpleañ os ( &mut self ) {
self . edad += 1 ;
println! ( "¡Feliz cumpleaños! Ahora tienes {} años" , self . edad);
}
fn es_adulto ( & self ) -> bool {
self . edad >= 18
}
}
fn main () {
let mut persona = Persona :: nueva ( String :: from ( "Ana" ), 17 );
persona . saludar ();
println! ( "¿Es adulto? {}" , persona . es_adulto ());
persona . cumpleañ os ();
println! ( "¿Es adulto ahora? {}" , persona . es_adulto ());
}
Ownership y Structs
Structs con Referencias
// Struct que contiene una referencia necesita lifetimes (lo veremos más adelante)
struct Producto <' a > {
nombre : & ' a str ,
precio : f64 ,
}
fn main () {
let nombre_producto = String :: from ( "Laptop" );
let producto = Producto {
nombre : & nombre_producto,
precio : 999.99 ,
};
println! ( "Producto: {} - ${}" , producto . nombre, producto . precio);
}
Structs que Poseen Sus Datos
#[derive( Debug , Clone )]
struct Libro {
titulo : String ,
autor : String ,
paginas : u32 ,
}
impl Libro {
fn nuevo (titulo : & str , autor : & str , paginas : u32 ) -> Libro {
Libro {
titulo : titulo . to_string (),
autor : autor . to_string (),
paginas,
}
}
fn descripcion ( & self ) -> String {
format! ( "'{}' por {} ({} páginas)" , self . titulo, self . autor, self . paginas)
}
// Método que consume y retorna una nueva versión
fn editar_titulo ( mut self , nuevo_titulo : & str ) -> Libro {
self . titulo = nuevo_titulo . to_string ();
self
}
}
fn main () {
let libro1 = Libro :: nuevo ( "1984" , "George Orwell" , 328 );
println! ( "Libro original: {}" , libro1 . descripcion ());
// Clonar para mantener el original
let libro2 = libro1 . clone () . editar_titulo ( "Animal Farm" );
println! ( "Libro original: {}" , libro1 . descripcion ());
println! ( "Libro editado: {}" , libro2 . descripcion ());
}
Debug y Display
Derivando Debug
#[derive( Debug )]
struct Coordenada {
x : f64 ,
y : f64 ,
}
fn main () {
let punto = Coordenada { x : 3.0 , y : 4.0 };
// Debug formatting
println! ( "Debug: {:?}" , punto);
println! ( "Pretty debug: {:#?}" , punto);
}
Implementando Display
use std :: fmt;
struct Temperatura {
celsius : f64 ,
}
impl fmt :: Display for Temperatura {
fn fmt ( & self , f : &mut fmt :: Formatter ) -> fmt :: Result {
write! (f, "{}°C" , self . celsius)
}
}
impl Temperatura {
fn nueva (celsius : f64 ) -> Temperatura {
Temperatura { celsius }
}
fn fahrenheit ( & self ) -> f64 {
self . celsius * 9.0 / 5.0 + 32.0
}
}
fn main () {
let temp = Temperatura :: nueva ( 25.0 );
println! ( "Temperatura: {}" , temp); // Usa Display
println! ( "En Fahrenheit: {:.1}°F" , temp . fahrenheit ());
}
Ejercicios Prácticos
Ejercicio 1: Sistema de Cuentas Bancarias
#[derive( Debug )]
struct CuentaBancaria {
numero : String ,
titular : String ,
saldo : f64 ,
}
impl CuentaBancaria {
fn nueva (numero : String , titular : String ) -> CuentaBancaria {
CuentaBancaria {
numero,
titular,
saldo : 0.0 ,
}
}
fn depositar ( &mut self , cantidad : f64 ) -> Result <(), String > {
if cantidad <= 0.0 {
return Err ( "La cantidad debe ser positiva" . to_string ());
}
self . saldo += cantidad;
Ok (())
}
fn retirar ( &mut self , cantidad : f64 ) -> Result <(), String > {
if cantidad <= 0.0 {
return Err ( "La cantidad debe ser positiva" . to_string ());
}
if cantidad > self . saldo {
return Err ( "Saldo insuficiente" . to_string ());
}
self . saldo -= cantidad;
Ok (())
}
fn consultar_saldo ( & self ) -> f64 {
self . saldo
}
fn transferir ( &mut self , otra_cuenta : &mut CuentaBancaria , cantidad : f64 ) -> Result <(), String > {
self . retirar (cantidad) ? ;
otra_cuenta . depositar (cantidad) ? ;
Ok (())
}
}
fn main () {
let mut cuenta1 = CuentaBancaria :: nueva (
"12345" . to_string (),
"Juan Pérez" . to_string ()
);
let mut cuenta2 = CuentaBancaria :: nueva (
"67890" . to_string (),
"María García" . to_string ()
);
// Operaciones
cuenta1 . depositar ( 1000.0 ) . unwrap ();
println! ( "Saldo cuenta1: ${}" , cuenta1 . consultar_saldo ());
cuenta1 . retirar ( 200.0 ) . unwrap ();
println! ( "Después de retirar: ${}" , cuenta1 . consultar_saldo ());
// Transferencia
cuenta1 . transferir ( &mut cuenta2, 300.0 ) . unwrap ();
println! ( "Cuenta1: ${}" , cuenta1 . consultar_saldo ());
println! ( "Cuenta2: ${}" , cuenta2 . consultar_saldo ());
}
Ejercicio 2: Sistema de Biblioteca
#[derive( Debug , Clone )]
struct Libro {
id : u32 ,
titulo : String ,
autor : String ,
disponible : bool ,
}
#[derive( Debug )]
struct Biblioteca {
libros : Vec < Libro >,
siguiente_id : u32 ,
}
impl Biblioteca {
fn nueva () -> Biblioteca {
Biblioteca {
libros : Vec :: new (),
siguiente_id : 1 ,
}
}
fn agregar_libro ( &mut self , titulo : String , autor : String ) {
let libro = Libro {
id : self . siguiente_id,
titulo,
autor,
disponible : true ,
};
self . libros . push (libro);
self . siguiente_id += 1 ;
}
fn buscar_por_titulo ( & self , titulo : & str ) -> Vec < & Libro > {
self . libros
. iter ()
. filter ( | libro | libro . titulo . contains (titulo))
. collect ()
}
fn prestar_libro ( &mut self , id : u32 ) -> Result <(), String > {
if let Some (libro) = self . libros . iter_mut () . find ( | l | l . id == id) {
if libro . disponible {
libro . disponible = false ;
Ok (())
} else {
Err ( "El libro ya está prestado" . to_string ())
}
} else {
Err ( "Libro no encontrado" . to_string ())
}
}
fn devolver_libro ( &mut self , id : u32 ) -> Result <(), String > {
if let Some (libro) = self . libros . iter_mut () . find ( | l | l . id == id) {
if ! libro . disponible {
libro . disponible = true ;
Ok (())
} else {
Err ( "El libro no está prestado" . to_string ())
}
} else {
Err ( "Libro no encontrado" . to_string ())
}
}
fn listar_disponibles ( & self ) -> Vec < & Libro > {
self . libros
. iter ()
. filter ( | libro | libro . disponible)
. collect ()
}
}
fn main () {
let mut biblioteca = Biblioteca :: nueva ();
// Agregar libros
biblioteca . agregar_libro ( "1984" . to_string (), "George Orwell" . to_string ());
biblioteca . agregar_libro ( "El Quijote" . to_string (), "Cervantes" . to_string ());
biblioteca . agregar_libro ( "Cien años de soledad" . to_string (), "García Márquez" . to_string ());
println! ( "Libros disponibles:" );
for libro in biblioteca . listar_disponibles () {
println! ( " {}: '{}' por {}" , libro . id, libro . titulo, libro . autor);
}
// Prestar un libro
biblioteca . prestar_libro ( 1 ) . unwrap ();
println! ( " \n Después de prestar libro ID 1:" );
for libro in biblioteca . listar_disponibles () {
println! ( " {}: '{}' por {}" , libro . id, libro . titulo, libro . autor);
}
// Buscar libros
let resultados = biblioteca . buscar_por_titulo ( "años" );
println! ( " \n Buscar 'años':" );
for libro in resultados {
println! ( " {}: '{}' por {}" , libro . id, libro . titulo, libro . autor);
}
}
Ejercicio 3: Juego de Cartas Simple
#[derive( Debug , Clone , Copy , PartialEq )]
enum Palo {
Corazones ,
Diamantes ,
Tréboles,
Espadas ,
}
#[derive( Debug , Clone , Copy )]
enum Valor {
As ,
Dos , Tres , Cuatro , Cinco , Seis , Siete , Ocho , Nueve , Diez ,
J , Q , K ,
}
#[derive( Debug , Clone , Copy )]
struct Carta {
palo : Palo ,
valor : Valor ,
}
#[derive( Debug )]
struct Baraja {
cartas : Vec < Carta >,
}
impl Carta {
fn nueva (palo : Palo , valor : Valor ) -> Carta {
Carta { palo, valor }
}
fn valor_numerico ( & self ) -> u32 {
match self . valor {
Valor :: As => 1 ,
Valor :: Dos => 2 ,
Valor :: Tres => 3 ,
Valor :: Cuatro => 4 ,
Valor :: Cinco => 5 ,
Valor :: Seis => 6 ,
Valor :: Siete => 7 ,
Valor :: Ocho => 8 ,
Valor :: Nueve => 9 ,
Valor :: Diez => 10 ,
Valor :: J => 11 ,
Valor :: Q => 12 ,
Valor :: K => 13 ,
}
}
}
impl Baraja {
fn nueva () -> Baraja {
let mut cartas = Vec :: new ();
let palos = [ Palo :: Corazones , Palo :: Diamantes , Palo :: Tréboles, Palo :: Espadas ];
let valores = [
Valor :: As , Valor :: Dos , Valor :: Tres , Valor :: Cuatro , Valor :: Cinco ,
Valor :: Seis , Valor :: Siete , Valor :: Ocho , Valor :: Nueve , Valor :: Diez ,
Valor :: J , Valor :: Q , Valor :: K ,
];
for palo in palos {
for valor in valores {
cartas . push ( Carta :: nueva (palo, valor));
}
}
Baraja { cartas }
}
fn barajar ( &mut self ) {
// Implementación simple de mezcla
use std :: collections :: HashMap ;
let mut temp : HashMap < usize , Carta > = HashMap :: new ();
for (i, carta) in self . cartas . iter () . enumerate () {
temp . insert (i, * carta);
}
// En una implementación real, usarías rand crate
self . cartas . reverse (); // Simplificación para el ejemplo
}
fn repartir ( &mut self ) -> Option < Carta > {
self . cartas . pop ()
}
fn cartas_restantes ( & self ) -> usize {
self . cartas . len ()
}
}
fn main () {
let mut baraja = Baraja :: nueva ();
println! ( "Baraja creada con {} cartas" , baraja . cartas_restantes ());
baraja . barajar ();
println! ( "Baraja mezclada" );
// Repartir algunas cartas
for i in 1 ..= 5 {
if let Some (carta) = baraja . repartir () {
println! ( "Carta {}: {:?} de {:?} (valor: {})" ,
i, carta . valor, carta . palo, carta . valor_numerico ());
}
}
println! ( "Cartas restantes: {}" , baraja . cartas_restantes ());
}
Puntos Clave para Recordar
Las structs agrupan datos relacionados en un solo tipo
Usa impl para definir métodos y funciones asociadas
&self para métodos de lectura , &mut self para modificación, self para consumir
Field init shorthand cuando el nombre del campo coincide con la variable
Struct update syntax para crear structs basadas en otras
Tuple structs para tipos simples con nombre
Unit structs para tipos sin datos (útiles para traits)
Múltiples bloques impl están permitidos
Deriva traits comunes como Debug, Clone, Copy cuando sea apropiado
Anterior
Borrowing
Siguiente
Enums Pattern Matching