En esta página

Enums y pattern matching

15 min lectura TextoCap. 3 — Tipos compuestos

Enums: representar múltiples formas

Los enums (enumeraciones) en Rust son mucho más poderosos que en la mayoría de lenguajes. Cada variante puede llevar datos distintos, haciendo que los enums de Rust sean equivalentes a los "tipos suma" o "union types" de la teoría de tipos.

Enums básicos

#[derive(Debug)]
enum DiaSemana {
    Lunes,
    Martes,
    Miércoles,
    Jueves,
    Viernes,
    Sábado,
    Domingo,
}

fn es_laboral(dia: &DiaSemana) -> bool {
    matches!(dia, DiaSemana::Lunes | DiaSemana::Martes | DiaSemana::Miércoles 
             | DiaSemana::Jueves | DiaSemana::Viernes)
}

fn main() {
    let hoy = DiaSemana::Miércoles;
    println!("Hoy: {:?}", hoy);
    println!("¿Es laboral? {}", es_laboral(&hoy));
}

Enums con datos

La característica más poderosa: cada variante puede llevar distintos tipos de datos:

#[derive(Debug)]
enum Mensaje {
    Salir,                              // Sin datos
    Mover { x: i32, y: i32 },          // Struct anónimo
    Texto(String),                      // Un String
    Color(u8, u8, u8),                  // Tres u8 (RGB)
    Adjunto { nombre: String, bytes: Vec<u8> }, // Struct con Vec
}

impl Mensaje {
    fn procesar(&self) {
        match self {
            Mensaje::Salir => println!("Cerrando aplicación..."),
            Mensaje::Mover { x, y } => println!("Moviendo a ({x}, {y})"),
            Mensaje::Texto(t) => println!("Texto recibido: {t}"),
            Mensaje::Color(r, g, b) => println!("Color: rgb({r}, {g}, {b})"),
            Mensaje::Adjunto { nombre, bytes } => {
                println!("Adjunto '{}' ({} bytes)", nombre, bytes.len())
            }
        }
    }
}

fn main() {
    let mensajes = vec![
        Mensaje::Mover { x: 10, y: 20 },
        Mensaje::Texto(String::from("¡Hola!")),
        Mensaje::Color(255, 128, 0),
        Mensaje::Adjunto {
            nombre: String::from("foto.jpg"),
            bytes: vec![0xFF, 0xD8, 0xFF],
        },
        Mensaje::Salir,
    ];
    
    for m in &mensajes {
        m.procesar();
    }
}

Option: adiós al null

Rust no tiene null. En su lugar, usa el enum Option<T> de la biblioteca estándar:

enum Option<T> {
    Some(T),  // Hay un valor
    None,     // No hay valor
}
fn encontrar_indice(lista: &[i32], objetivo: i32) -> Option<usize> {
    for (i, &n) in lista.iter().enumerate() {
        if n == objetivo {
            return Some(i);
        }
    }
    None
}

fn dividir(a: f64, b: f64) -> Option<f64> {
    if b == 0.0 { None } else { Some(a / b) }
}

fn main() {
    let numeros = [3, 1, 4, 1, 5, 9, 2, 6];
    
    match encontrar_indice(&numeros, 5) {
        Some(i) => println!("Encontrado en índice {i}"),
        None    => println!("No encontrado"),
    }
    
    // Métodos convenientes de Option
    let resultado = dividir(10.0, 3.0);
    println!("Resultado: {:.4}", resultado.unwrap_or(0.0));
    
    let nada = dividir(5.0, 0.0);
    println!("División por cero: {}", nada.is_none());
    
    // map: transformar el valor si existe
    let doble = resultado.map(|r| r * 2.0);
    println!("Doble: {:.4}", doble.unwrap_or(0.0));
    
    // and_then: encadenar operaciones que pueden fallar
    let cadena = Some("42");
    let numero: Option<i32> = cadena.and_then(|s| s.parse().ok());
    println!("Número: {:?}", numero);
}

Result: manejo de errores

Result<T, E> es el otro enum fundamental de la biblioteca estándar:

enum Result<T, E> {
    Ok(T),   // Operación exitosa con valor T
    Err(E),  // Error de tipo E
}
use std::num::ParseIntError;

fn parsear_numero(s: &str) -> Result<i32, ParseIntError> {
    s.trim().parse::<i32>()
}

fn main() {
    let casos = ["42", " -7 ", "hola", "999999999999"];
    
    for caso in &casos {
        match parsear_numero(caso) {
            Ok(n)  => println!("'{caso}' → {n}"),
            Err(e) => println!("'{caso}' → Error: {e}"),
        }
    }
}

match: el corazón del pattern matching

match es el operador más poderoso de Rust para trabajar con enums. Es exhaustivo: debe cubrir todos los casos posibles:

fn describir_numero(n: i32) -> &'static str {
    match n {
        i32::MIN..=-1 => "negativo",
        0             => "cero",
        1..=9         => "dígito",
        10..=99       => "dos dígitos",
        100..=999     => "tres dígitos",
        _             => "grande",
    }
}

fn clasificar_char(c: char) -> &'static str {
    match c {
        'a'..='z' | 'á' | 'é' | 'í' | 'ó' | 'ú' | 'ñ' => "letra minúscula",
        'A'..='Z' | 'Á' | 'É' | 'Í' | 'Ó' | 'Ú' | 'Ñ' => "letra mayúscula",
        '0'..='9' => "dígito",
        ' ' | '\t' | '\n' => "espacio",
        _ => "símbolo",
    }
}

fn main() {
    for n in [-5, 0, 7, 42, 500, 10_000] {
        println!("{n}: {}", describir_numero(n));
    }
    
    for c in ['a', 'Z', '5', ' ', '@', 'ñ'] {
        println!("'{c}': {}", clasificar_char(c));
    }
}

Guards en match

Los guards permiten añadir condiciones adicionales a los brazos del match:

fn evaluar_temperatura(temp: f64) -> &'static str {
    match temp {
        t if t < -30.0 => "peligrosamente frío",
        t if t < 0.0   => "bajo cero",
        t if t < 15.0  => "frío",
        t if t < 25.0  => "confortable",
        t if t < 35.0  => "caluroso",
        t if t < 40.0  => "muy caluroso",
        _              => "peligrosamente caliente",
    }
}

fn main() {
    for temp in [-40.0, -5.0, 10.0, 22.0, 33.0, 38.0, 42.0] {
        println!("{temp:.1}°C: {}", evaluar_temperatura(temp));
    }
}

if let y while let: pattern matching conciso

Cuando solo te interesa un caso del enum:

fn main() {
    let configuracion: Option<u32> = Some(8080);
    
    // Con match (verboso para un solo caso):
    match configuracion {
        Some(puerto) => println!("Puerto configurado: {puerto}"),
        None => (), // No hacemos nada
    }
    
    // Con if let (más conciso):
    if let Some(puerto) = configuracion {
        println!("Puerto: {puerto}");
    }
    
    // if let con else:
    if let Some(puerto) = configuracion {
        println!("Puerto custom: {puerto}");
    } else {
        println!("Usando puerto por defecto: 3000");
    }
    
    // while let: iterar hasta que el patrón no coincida
    let mut pila = vec![1, 2, 3, 4, 5];
    while let Some(cima) = pila.pop() {
        print!("{cima} ");
    }
    println!();
    
    // Encadenando if let
    let valor: Result<Option<i32>, String> = Ok(Some(42));
    if let Ok(Some(n)) = valor {
        println!("Valor anidado: {n}");
    }
}

Patrones anidados y destructuring

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

fn main() {
    let punto = Punto { x: 3, y: -5 };
    
    // Destructuring en match
    let descripcion = match punto {
        Punto { x: 0, y: 0 } => "origen",
        Punto { x, y: 0 }    => "en el eje X",
        Punto { x: 0, y }    => "en el eje Y",
        Punto { x, y } if x == y => "en la diagonal",
        _                    => "punto arbitrario",
    };
    println!("{descripcion}");
    
    // Binding con @
    let n = 15;
    match n {
        x @ 1..=10 => println!("{x} está entre 1 y 10"),
        x @ 11..=20 => println!("{x} está entre 11 y 20"),
        x => println!("{x} está fuera del rango"),
    }
    
    // Tuplas en match
    let coordenadas = (1, -1);
    match coordenadas {
        (0, 0) => println!("Origen"),
        (x, 0) | (0, x) => println!("En eje con {x}"),
        (x, y) if x == -y => println!("Antidiagonal"),
        (x, y) => println!("({x}, {y})"),
    }
}

Los enums y el pattern matching hacen que el código Rust sea extraordinariamente expresivo. La próxima lección cubre las colecciones de la biblioteca estándar — Vec, String y HashMap.

match es exhaustivo
El compilador verifica que todos los casos posibles de un enum estén cubiertos en un match. Si añades una nueva variante al enum, el compilador te avisará en todos los lugares donde hay un match que no la maneja. Esto previene bugs de 'caso olvidado'.
Option<T> reemplaza a null
Rust no tiene null. En su lugar usa Option<T> con variantes Some(T) para un valor presente y None para ausencia. Esto obliga al programador a manejar el caso de 'sin valor' explícitamente, eliminando los NullPointerExceptions del mundo Rust.
rust
#[derive(Debug)]
enum Forma {
    Circulo(f64),
    Rectangulo { ancho: f64, alto: f64 },
    Triangulo(f64, f64, f64),
}

impl Forma {
    fn area(&self) -> f64 {
        match self {
            Forma::Circulo(r) => std::f64::consts::PI * r * r,
            Forma::Rectangulo { ancho, alto } => ancho * alto,
            Forma::Triangulo(a, b, c) => {
                // Fórmula de Herón
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }

    fn nombre(&self) -> &str {
        match self {
            Forma::Circulo(_)        => "Círculo",
            Forma::Rectangulo { .. } => "Rectángulo",
            Forma::Triangulo(..)     => "Triángulo",
        }
    }
}

fn main() {
    let formas: Vec<Forma> = vec![
        Forma::Circulo(5.0),
        Forma::Rectangulo { ancho: 4.0, alto: 6.0 },
        Forma::Triangulo(3.0, 4.0, 5.0),
    ];

    for forma in &formas {
        println!("{}: área = {:.4}", forma.nombre(), forma.area());
    }
}