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!("\nDespué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!("\nBuscar '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
Próximo Paso
En el siguiente capítulo exploraremos Enums y Pattern Matching, que junto con structs forman la base del sistema de tipos de Rust y nos permiten modelar datos de manera más expresiva y segura.