Enums en Rust: mucho más que constantes, modela datos reales con variantes

Los enums de Rust no son simples constantes numéricas como en C o Java. Cada variante puede llevar datos de tipos distintos, lo que los convierte en algebraic data types (tipos de datos algebraicos). Son una de las características más expresivas del lenguaje y la base de Option y Result.

Enum básico sin datos

enum Direccion {
    Norte,
    Sur,
    Este,
    Oeste,
}

fn mover(d: Direccion) {
    match d {
        Direccion::Norte => println!("Subiendo"),
        Direccion::Sur   => println!("Bajando"),
        Direccion::Este  => println!("Derecha"),
        Direccion::Oeste => println!("Izquierda"),
    }
}

fn main() {
    mover(Direccion::Norte);
}

Variantes con datos: IpAddr

Cada variante puede llevar datos distintos. No necesitas un struct separado:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let local   = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));

    match local {
        IpAddr::V4(a, b, c, d) => println!("{}.{}.{}.{}", a, b, c, d),
        IpAddr::V6(s)          => println!("{}", s),
    }
}

Cada variante es una especie de constructor. IpAddr::V4 recibe cuatro u8; IpAddr::V6 recibe un String. Los tipos son completamente independientes.

Variantes con structs anónimos

enum Mensaje {
    Salir,
    Mover { x: i32, y: i32 },
    Escribir(String),
    CambiarColor(u8, u8, u8),
}

fn procesar(m: Mensaje) {
    match m {
        Mensaje::Salir             => println!("Salir"),
        Mensaje::Mover { x, y }   => println!("Mover a ({}, {})", x, y),
        Mensaje::Escribir(texto)  => println!("Escribir: {}", texto),
        Mensaje::CambiarColor(r, g, b) => println!("Color: #{:02X}{:02X}{:02X}", r, g, b),
    }
}

Una sola variante puede combinar campos con nombre (como un struct) o posicionales (como una tupla).

Métodos en enums con impl

Al igual que los structs, los enums pueden tener métodos:

enum Moneda {
    Penique,
    Niquel,
    Dime,
    Cuarto,
}

impl Moneda {
    fn valor_en_centimos(&self) -> u32 {
        match self {
            Moneda::Penique  => 1,
            Moneda::Niquel   => 5,
            Moneda::Dime     => 10,
            Moneda::Cuarto   => 25,
        }
    }
}

fn main() {
    let m = Moneda::Cuarto;
    println!("{} centimos", m.valor_en_centimos());
}

Por qué se llaman algebraic data types

En teoría de tipos, un product type combina varios valores a la vez (como un struct: tiene campo A y campo B). Un sum type representa alternativas (tiene campo A o campo B). Los enums de Rust son sum types: un valor del enum es exactamente una de sus variantes, y solo lleva los datos de esa variante.

Esta dualidad (structs = product, enums = sum) te permite modelar cualquier dominio de datos con precisión:

// Árbol binario modelado con un enum
enum Arbol {
    Hoja(i32),
    Nodo { valor: i32, izq: Box<Arbol>, der: Box<Arbol> },
}

fn suma(a: &Arbol) -> i32 {
    match a {
        Arbol::Hoja(v)           => *v,
        Arbol::Nodo { valor, izq, der } => valor + suma(izq) + suma(der),
    }
}

Option y Result son enums

Dos de los tipos más usados de la stdlib son enums:

// De la stdlib
enum Option<T> {
    None,
    Some(T),
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option reemplaza al null; Result reemplaza a las excepciones. Ambos se tratan en profundidad en los siguientes artículos.

Resumen

  • Los enums de Rust son algebraic data types: cada variante puede llevar datos de tipos distintos.
  • Las variantes pueden ser simples, con tupla, con struct anónimo o sin datos.
  • Los enums pueden tener métodos con impl.
  • Option<T> y Result<T, E> son enums de la stdlib.

El siguiente artículo profundiza en Option<T>: cómo usarla con match, if let, unwrap_or, map y el operador ? para manejar la ausencia de valores sin null.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP