Enums y Pattern Matching
Los enums (enumeraciones) en Rust son extremadamente poderosos y van mucho más allá de las enumeraciones simples de otros lenguajes. Permiten definir tipos que pueden ser una de varias variantes, y cada variante puede contener diferentes tipos y cantidades de datos.
Definiendo Enums
Enum Básico
#[derive(Debug)]
enum DireccionIP {
V4,
V6,
}
fn main() {
let casa = DireccionIP::V4;
let oficina = DireccionIP::V6;
println!("Casa: {:?}", casa);
println!("Oficina: {:?}", oficina);
}
Enums con Datos
#[derive(Debug)]
enum DireccionIP {
V4(u8, u8, u8, u8),
V6(String),
}
fn main() {
let casa = DireccionIP::V4(127, 0, 0, 1);
let oficina = DireccionIP::V6(String::from("::1"));
println!("Casa: {:?}", casa);
println!("Oficina: {:?}", oficina);
}
Enums con Diferentes Tipos de Datos
#[derive(Debug)]
enum Mensaje {
Salir,
Mover { x: i32, y: i32 },
Escribir(String),
CambiarColor(i32, i32, i32),
}
fn main() {
let mensajes = vec![
Mensaje::Salir,
Mensaje::Mover { x: 10, y: 15 },
Mensaje::Escribir(String::from("Hola mundo")),
Mensaje::CambiarColor(255, 0, 128),
];
for mensaje in mensajes {
procesar_mensaje(mensaje);
}
}
fn procesar_mensaje(msg: Mensaje) {
match msg {
Mensaje::Salir => println!("Saliendo del programa"),
Mensaje::Mover { x, y } => println!("Moviendo a ({x}, {y})"),
Mensaje::Escribir(texto) => println!("Escribiendo: {texto}"),
Mensaje::CambiarColor(r, g, b) => println!("Cambiando color a RGB({r}, {g}, {b})"),
}
}
Enums con Métodos
#[derive(Debug)]
enum Forma {
Rectangulo { ancho: f64, alto: f64 },
Circulo { radio: f64 },
Triangulo { base: f64, altura: f64 },
}
impl Forma {
fn area(&self) -> f64 {
match self {
Forma::Rectangulo { ancho, alto } => ancho * alto,
Forma::Circulo { radio } => std::f64::consts::PI * radio * radio,
Forma::Triangulo { base, altura } => 0.5 * base * altura,
}
}
fn perimetro(&self) -> f64 {
match self {
Forma::Rectangulo { ancho, alto } => 2.0 * (ancho + alto),
Forma::Circulo { radio } => 2.0 * std::f64::consts::PI * radio,
Forma::Triangulo { base, altura } => {
// Asumimos triángulo rectángulo para simplificar
let hipotenusa = (base * base + altura * altura).sqrt();
base + altura + hipotenusa
}
}
}
fn descripcion(&self) -> String {
match self {
Forma::Rectangulo { ancho, alto } => {
format!("Rectángulo {}x{}", ancho, alto)
}
Forma::Circulo { radio } => {
format!("Círculo con radio {}", radio)
}
Forma::Triangulo { base, altura } => {
format!("Triángulo {}x{}", base, altura)
}
}
}
}
fn main() {
let formas = vec![
Forma::Rectangulo { ancho: 10.0, alto: 5.0 },
Forma::Circulo { radio: 3.0 },
Forma::Triangulo { base: 4.0, altura: 3.0 },
];
for forma in &formas {
println!("{}", forma.descripcion());
println!(" Área: {:.2}", forma.area());
println!(" Perímetro: {:.2}", forma.perimetro());
println!();
}
}
El Enum Option
Option<T> es uno de los enums más importantes en Rust, usado para valores que pueden existir o no:
fn dividir(a: f64, b: f64) -> Option<f64> {
if b != 0.0 {
Some(a / b)
} else {
None
}
}
fn main() {
let resultados = vec![
dividir(10.0, 2.0),
dividir(5.0, 0.0),
dividir(8.0, 4.0),
];
for (i, resultado) in resultados.iter().enumerate() {
match resultado {
Some(valor) => println!("Resultado {}: {:.2}", i + 1, valor),
None => println!("Resultado {}: División por cero", i + 1),
}
}
// Métodos útiles de Option
let numero = Some(5);
// unwrap: extrae el valor o entra en pánico
println!("Valor: {}", numero.unwrap());
// unwrap_or: valor por defecto si es None
let nada: Option<i32> = None;
println!("Valor o defecto: {}", nada.unwrap_or(0));
// map: transforma el valor si existe
let doble = numero.map(|x| x * 2);
println!("Doble: {:?}", doble);
// and_then: encadena operaciones que retornan Option
let cuadrado_si_par = numero.and_then(|x| {
if x % 2 == 0 {
Some(x * x)
} else {
None
}
});
println!("Cuadrado si par: {:?}", cuadrado_si_par);
}
El Enum Result<T, E>
Result<T, E> se usa para operaciones que pueden fallar:
#[derive(Debug)]
enum MiError {
DivisionPorCero,
NumeroNegativo,
Overflow,
}
fn operacion_segura(a: i32, b: i32) -> Result<i32, MiError> {
if b == 0 {
return Err(MiError::DivisionPorCero);
}
if a < 0 || b < 0 {
return Err(MiError::NumeroNegativo);
}
let resultado = a.checked_mul(b);
match resultado {
Some(valor) => Ok(valor),
None => Err(MiError::Overflow),
}
}
fn main() {
let operaciones = vec![
(4, 5),
(10, 0),
(-5, 3),
(1000000, 1000000),
];
for (a, b) in operaciones {
match operacion_segura(a, b) {
Ok(resultado) => println!("{} * {} = {}", a, b, resultado),
Err(error) => println!("{} * {} = Error: {:?}", a, b, error),
}
}
// Métodos útiles de Result
let resultado = operacion_segura(3, 4);
// unwrap: extrae el valor o entra en pánico
println!("Resultado: {}", resultado.unwrap());
// expect: como unwrap pero con mensaje personalizado
let resultado2 = operacion_segura(2, 6);
println!("Resultado: {}", resultado2.expect("Error en la operación"));
// unwrap_or: valor por defecto en caso de error
let resultado3 = operacion_segura(5, 0);
println!("Resultado o defecto: {}", resultado3.unwrap_or(-1));
}
Pattern Matching Avanzado
Guards en match
fn clasificar_numero(num: i32) -> String {
match num {
n if n < 0 => "Negativo".to_string(),
0 => "Cero".to_string(),
n if n > 0 && n <= 10 => "Pequeño positivo".to_string(),
n if n > 10 && n <= 100 => "Medio".to_string(),
n if n % 2 == 0 => "Grande y par".to_string(),
_ => "Grande e impar".to_string(),
}
}
fn main() {
let numeros = vec![-5, 0, 3, 15, 50, 123, 100];
for num in numeros {
println!("{}: {}", num, clasificar_numero(num));
}
}
Destructuring Complejo
#[derive(Debug)]
enum Evento {
Click { x: i32, y: i32, boton: Boton },
Teclado { tecla: char, modificadores: Vec<String> },
Scroll { delta: i32 },
}
#[derive(Debug)]
enum Boton {
Izquierdo,
Derecho,
Medio,
}
fn manejar_evento(evento: Evento) {
match evento {
// Destructuring específico
Evento::Click { x: 0, y: 0, boton } => {
println!("Click en origen con botón {:?}", boton);
}
// Destructuring con guards
Evento::Click { x, y, boton: Boton::Izquierdo } if x > 100 && y > 100 => {
println!("Click izquierdo en área especial ({}, {})", x, y);
}
// Destructuring general
Evento::Click { x, y, boton } => {
println!("Click {:?} en ({}, {})", boton, x, y);
}
// Destructuring de Vec
Evento::Teclado { tecla, modificadores } if modificadores.len() > 0 => {
println!("Tecla '{}' con modificadores: {:?}", tecla, modificadores);
}
Evento::Teclado { tecla, .. } => {
println!("Tecla simple: '{}'", tecla);
}
Evento::Scroll { delta } => {
let direccion = if delta > 0 { "arriba" } else { "abajo" };
println!("Scroll {} ({})", direccion, delta.abs());
}
}
}
fn main() {
let eventos = vec![
Evento::Click { x: 0, y: 0, boton: Boton::Izquierdo },
Evento::Click { x: 150, y: 200, boton: Boton::Izquierdo },
Evento::Click { x: 50, y: 75, boton: Boton::Derecho },
Evento::Teclado {
tecla: 'a',
modificadores: vec!["Ctrl".to_string(), "Shift".to_string()]
},
Evento::Teclado { tecla: 'x', modificadores: vec![] },
Evento::Scroll { delta: -5 },
];
for evento in eventos {
manejar_evento(evento);
}
}
if let y while let
Para casos donde solo te interesa un patrón específico:
fn main() {
let config_max = Some(3u8);
// En lugar de match completo
if let Some(max) = config_max {
println!("El máximo configurado es: {}", max);
}
// Con else
let numero: Option<i32> = None;
if let Some(n) = numero {
println!("El número es: {}", n);
} else {
println!("No hay número");
}
// while let para procesar hasta que no haya más elementos
let mut pila = vec![1, 2, 3];
while let Some(tope) = pila.pop() {
println!("Procesando: {}", tope);
}
// Procesando Result con if let
let resultado: Result<i32, &str> = Ok(42);
if let Ok(valor) = resultado {
println!("Éxito: {}", valor);
}
if let Err(error) = resultado {
println!("Error: {}", error);
}
}
Ejercicios Prácticos
Ejercicio 1: Sistema de Notificaciones
#[derive(Debug)]
enum Notificacion {
Email {
destinatario: String,
asunto: String,
cuerpo: String
},
SMS {
numero: String,
mensaje: String
},
Push {
titulo: String,
contenido: String,
badge: Option<u32>
},
}
impl Notificacion {
fn enviar(&self) -> Result<(), String> {
match self {
Notificacion::Email { destinatario, asunto, .. } => {
if destinatario.contains('@') {
println!("📧 Email enviado a {}: {}", destinatario, asunto);
Ok(())
} else {
Err("Email inválido".to_string())
}
}
Notificacion::SMS { numero, mensaje } => {
if numero.len() >= 10 {
println!("📱 SMS enviado a {}: {}", numero, mensaje);
Ok(())
} else {
Err("Número de teléfono inválido".to_string())
}
}
Notificacion::Push { titulo, contenido, badge } => {
let badge_text = match badge {
Some(n) => format!(" [{}]", n),
None => String::new(),
};
println!("🔔 Push{}: {} - {}", badge_text, titulo, contenido);
Ok(())
}
}
}
fn costo(&self) -> f64 {
match self {
Notificacion::Email { .. } => 0.01,
Notificacion::SMS { .. } => 0.05,
Notificacion::Push { .. } => 0.02,
}
}
}
fn main() {
let notificaciones = vec![
Notificacion::Email {
destinatario: "usuario@ejemplo.com".to_string(),
asunto: "Bienvenido".to_string(),
cuerpo: "Gracias por registrarte".to_string(),
},
Notificacion::SMS {
numero: "123456789".to_string(),
mensaje: "Código de verificación: 1234".to_string(),
},
Notificacion::Push {
titulo: "Nueva actualización".to_string(),
contenido: "Hay funcionalidades nuevas disponibles".to_string(),
badge: Some(3),
},
];
let mut costo_total = 0.0;
for notificacion in ¬ificaciones {
match notificacion.enviar() {
Ok(()) => {
costo_total += notificacion.costo();
println!(" Costo: ${:.2}", notificacion.costo());
}
Err(error) => {
println!(" ❌ Error: {}", error);
}
}
println!();
}
println!("Costo total: ${:.2}", costo_total);
}
Ejercicio 2: Calculadora con Manejo de Errores
#[derive(Debug)]
enum OperacionMatematica {
Sumar(f64, f64),
Restar(f64, f64),
Multiplicar(f64, f64),
Dividir(f64, f64),
Potencia(f64, f64),
RaizCuadrada(f64),
}
#[derive(Debug)]
enum ErrorCalculo {
DivisionPorCero,
RaizNegativa,
Overflow,
ResultadoInvalido,
}
impl OperacionMatematica {
fn calcular(&self) -> Result<f64, ErrorCalculo> {
match self {
OperacionMatematica::Sumar(a, b) => Ok(a + b),
OperacionMatematica::Restar(a, b) => Ok(a - b),
OperacionMatematica::Multiplicar(a, b) => {
let resultado = a * b;
if resultado.is_infinite() {
Err(ErrorCalculo::Overflow)
} else {
Ok(resultado)
}
}
OperacionMatematica::Dividir(a, b) => {
if *b == 0.0 {
Err(ErrorCalculo::DivisionPorCero)
} else {
let resultado = a / b;
if resultado.is_nan() || resultado.is_infinite() {
Err(ErrorCalculo::ResultadoInvalido)
} else {
Ok(resultado)
}
}
}
OperacionMatematica::Potencia(base, exponente) => {
let resultado = base.powf(*exponente);
if resultado.is_nan() || resultado.is_infinite() {
Err(ErrorCalculo::ResultadoInvalido)
} else {
Ok(resultado)
}
}
OperacionMatematica::RaizCuadrada(n) => {
if *n < 0.0 {
Err(ErrorCalculo::RaizNegativa)
} else {
Ok(n.sqrt())
}
}
}
}
fn descripcion(&self) -> String {
match self {
OperacionMatematica::Sumar(a, b) => format!("{} + {}", a, b),
OperacionMatematica::Restar(a, b) => format!("{} - {}", a, b),
OperacionMatematica::Multiplicar(a, b) => format!("{} * {}", a, b),
OperacionMatematica::Dividir(a, b) => format!("{} / {}", a, b),
OperacionMatematica::Potencia(base, exp) => format!("{}^{}", base, exp),
OperacionMatematica::RaizCuadrada(n) => format!("√{}", n),
}
}
}
fn main() {
let operaciones = vec![
OperacionMatematica::Sumar(10.0, 5.0),
OperacionMatematica::Dividir(15.0, 3.0),
OperacionMatematica::Dividir(10.0, 0.0),
OperacionMatematica::RaizCuadrada(16.0),
OperacionMatematica::RaizCuadrada(-4.0),
OperacionMatematica::Potencia(2.0, 10.0),
OperacionMatematica::Multiplicar(f64::MAX, 2.0),
];
for operacion in operaciones {
print!("{} = ", operacion.descripcion());
match operacion.calcular() {
Ok(resultado) => println!("{:.2}", resultado),
Err(error) => {
let mensaje = match error {
ErrorCalculo::DivisionPorCero => "Error: División por cero",
ErrorCalculo::RaizNegativa => "Error: Raíz de número negativo",
ErrorCalculo::Overflow => "Error: Desbordamiento numérico",
ErrorCalculo::ResultadoInvalido => "Error: Resultado no válido",
};
println!("{}", mensaje);
}
}
}
}
Ejercicio 3: Sistema de Estados de Juego
#[derive(Debug, Clone)]
enum EstadoJuego {
Menu,
Jugando {
nivel: u32,
puntuacion: u32,
vidas: u32
},
Pausa {
nivel: u32,
puntuacion: u32,
vidas: u32
},
GameOver {
puntuacion_final: u32
},
Victoria {
puntuacion_final: u32,
tiempo: u32
},
}
#[derive(Debug)]
enum AccionJuego {
IniciarJuego,
PausarJuego,
ReanudarJuego,
GanarPunto(u32),
PerderVida,
CompletarNivel,
VolverAlMenu,
Reiniciar,
}
impl EstadoJuego {
fn procesar_accion(&mut self, accion: AccionJuego) -> Result<(), String> {
let nuevo_estado = match (&self, accion) {
// Desde Menu
(EstadoJuego::Menu, AccionJuego::IniciarJuego) => {
EstadoJuego::Jugando { nivel: 1, puntuacion: 0, vidas: 3 }
}
// Desde Jugando
(EstadoJuego::Jugando { nivel, puntuacion, vidas }, AccionJuego::PausarJuego) => {
EstadoJuego::Pausa { nivel: *nivel, puntuacion: *puntuacion, vidas: *vidas }
}
(EstadoJuego::Jugando { nivel, puntuacion, vidas }, AccionJuego::GanarPunto(puntos)) => {
EstadoJuego::Jugando {
nivel: *nivel,
puntuacion: puntuacion + puntos,
vidas: *vidas
}
}
(EstadoJuego::Jugando { nivel, puntuacion, vidas }, AccionJuego::PerderVida) => {
if *vidas <= 1 {
EstadoJuego::GameOver { puntuacion_final: *puntuacion }
} else {
EstadoJuego::Jugando {
nivel: *nivel,
puntuacion: *puntuacion,
vidas: vidas - 1
}
}
}
(EstadoJuego::Jugando { nivel, puntuacion, vidas }, AccionJuego::CompletarNivel) => {
if *nivel >= 10 {
EstadoJuego::Victoria { puntuacion_final: *puntuacion, tiempo: 300 }
} else {
EstadoJuego::Jugando {
nivel: nivel + 1,
puntuacion: puntuacion + 100, // Bonus por nivel
vidas: *vidas
}
}
}
// Desde Pausa
(EstadoJuego::Pausa { nivel, puntuacion, vidas }, AccionJuego::ReanudarJuego) => {
EstadoJuego::Jugando { nivel: *nivel, puntuacion: *puntuacion, vidas: *vidas }
}
// Acciones globales
(_, AccionJuego::VolverAlMenu) => EstadoJuego::Menu,
(_, AccionJuego::Reiniciar) => EstadoJuego::Menu,
// Combinaciones inválidas
(estado_actual, accion) => {
return Err(format!("Acción {:?} no válida en estado {:?}", accion, estado_actual));
}
};
*self = nuevo_estado;
Ok(())
}
fn descripcion(&self) -> String {
match self {
EstadoJuego::Menu => "En el menú principal".to_string(),
EstadoJuego::Jugando { nivel, puntuacion, vidas } => {
format!("Jugando - Nivel: {}, Puntuación: {}, Vidas: {}", nivel, puntuacion, vidas)
}
EstadoJuego::Pausa { nivel, puntuacion, vidas } => {
format!("En pausa - Nivel: {}, Puntuación: {}, Vidas: {}", nivel, puntuacion, vidas)
}
EstadoJuego::GameOver { puntuacion_final } => {
format!("Game Over - Puntuación final: {}", puntuacion_final)
}
EstadoJuego::Victoria { puntuacion_final, tiempo } => {
format!("¡Victoria! Puntuación: {}, Tiempo: {}s", puntuacion_final, tiempo)
}
}
}
}
fn main() {
let mut juego = EstadoJuego::Menu;
let acciones = vec![
AccionJuego::IniciarJuego,
AccionJuego::GanarPunto(50),
AccionJuego::GanarPunto(25),
AccionJuego::PausarJuego,
AccionJuego::ReanudarJuego,
AccionJuego::PerderVida,
AccionJuego::CompletarNivel,
AccionJuego::GanarPunto(75),
AccionJuego::CompletarNivel,
AccionJuego::VolverAlMenu,
];
println!("Estado inicial: {}", juego.descripcion());
println!();
for (i, accion) in acciones.iter().enumerate() {
println!("Acción {}: {:?}", i + 1, accion);
match juego.procesar_accion(accion.clone()) {
Ok(()) => println!(" ✅ {}", juego.descripcion()),
Err(error) => println!(" ❌ {}", error),
}
println!();
}
}
Puntos Clave para Recordar
- Los enums pueden contener datos de diferentes tipos y estructuras
Option<T> reemplaza valores nulos de forma segura
Result<T, E> maneja errores de forma explícita
match debe ser exhaustivo - cubrir todos los casos posibles
- Usa guards para condiciones adicionales en patterns
if let y while let simplifican casos específicos
- Los enums pueden tener métodos como las structs
- Destructuring permite extraer datos de variantes complejas
- Combina enums con structs para modelado de datos poderoso
Próximo Paso
En el siguiente capítulo exploraremos la Gestión de Errores en profundidad, incluyendo técnicas avanzadas con Result, propagación de errores, y cómo crear sistemas de manejo de errores robustos.