Enumerados - enum

Este capítulo se puede ver en YouTube en inglés: Parte 1, Parte 2, Parte 3 y Parte 4

La palabra reservada de Rust enum se usa para los tipos enumerados. Esta es la diferencia con struct:

  • Se utiliza struct cuando un tipo de datos debe representar una cosa Y otra cosa a la vez.
  • Se utiliza enum cuando un tipo de datos puede representar una cosa O alguna cosa diferente.

Las estructuras sirven para unir diferentes elementos en uno solo, mientras que los enumerados permiten que un tipo de datos represente a diferentes cosas en diferente momento.

Para declarar un enumerado se debe escribir enum seguido de un bloque {} con las diferentes opciones separadas por coma. Como en el caso de los struct la última opción puede llevar la coma o no. A continuación se crea un enumerado denominado CosasEnElCielo:

enum CosasEnElCielo {
    Sol,
    Estrellas,
}

fn main() {}

Es un enumerado, por lo tanto, cuando se cree un valor es necesario que se elija entre el Sol o las Estrellas. A cada elemento que forma parte del enumerado se le denomina variante.

// Crea el enumerado con dos variantes
enum CosasEnElCielo {
    Sol,
    Estrellas,
}

// Con esa función se usa un i32 para crear CosasEnElCielo.
fn crear_estadoEnElCielo(time: i32) -> CosasEnElCielo {
    match time {
        6..=18 => CosasEnElCielo::Sol, // Entre las 6 y 18 horas se ve el sol
        _ => CosasEnElCielo::Estrellas, // En otro caso se ven las estrellas
    }
}

// Con esta función se localiza el estado y se muestran las CosasEnElCielo.
fn comprobar_el_cielo(state: &CosasEnElCielo) {
    match state {
        CosasEnElCielo::Sol => println!("¡Puedo ver el sol!"),
        CosasEnElCielo::Estrellas => println!("¡Puedo ver las estrellas!")
    }
}

fn main() {
    let time = 8; // Son las ocho de la mañana
    let skystate = crear_estadoEnElCielo(time); // crear_estadoEnElCielo returns a CosasEnElCielo
    comprobar_el_cielo(&skystate); // Se pasa una referencia para que pueda leer el estado del cielo
}

Este código imprime ¡Puedo ver el sol!.

A cada enumerado, se le pueden añadir datos (como en los struts):

enum CosasEnElCielo {
    Sol(String), // Ahora cada variante tiene una cadena de texto
    Estrellas(String),
}

fn crear_estadoEnElCielo(time: i32) -> CosasEnElCielo {
    match time {
        6..=18 => CosasEnElCielo::Sol(String::from("¡Puedo ver el sol!")), // Da el valor aquí
        _ => CosasEnElCielo::Estrellas(String::from("¡Puedo ver las estrellas!")),
    }
}

fn comprobar_el_cielo(state: &CosasEnElCielo) {
    match state {
        CosasEnElCielo::Sol(description) => println!("{}", description), // recupera la descripción para que se pueda imprimir
        CosasEnElCielo::Estrellas(n) => println!("{}", n), // se puede usar cualquier variable n para obtener la descripción
    }
}

fn main() {
    let time = 8; // Son las ocho de la mañana
    let skystate = crear_estadoEnElCielo(time); // crear_estadoEnElCielo devuelve un elemento de CosasEnElCielo
    comprobar_el_cielo(&skystate); // Se pasa una referencia para que pueda leer el estado del cielo
}

Este código imprime lo mismo que antes ¡Puedo ver el sol!.

También se puede "importar" un enumerado para que no haya que escribir mucho. A continuación se muestra un ejemplo en se escribe Estado:: cada vez que se comprueba el "estado de ánimo":

enum Estado {
    Feliz,
    Cansado,
    NoEstoyMal,
    Enfadado,
}

fn comprueba_estado(mood: &Estado) -> i32 {
    let nivel_de_felicidad = match mood {
        Estado::Feliz => 10, // Se escribe Estado:: cada vez
        Estado::Cansado => 6,
        Estado::NoEstoyMal => 7,
        Estado::Enfadado => 2,
    };
    nivel_de_felicidad
}

fn main() {
    let my_mood = Estado::NoEstoyMal;
    let nivel_de_felicidad = comprueba_estado(&my_mood);
    println!("De 1 a 10, mi estado de felicidad es {}", nivel_de_felicidad);
}

El código anterior imprime De 1 a 10, mi estado de felicidad es 7. A continuación, el mismo código, pero importando el enumerado para tener que escribir menos. Para importar todo se utiliza *. Es el mismo carácter que para desrreferenciar, pero con un uso diferente.

enum Estado {
    Feliz,
    Cansado,
    NoEstoyMal,
    Enfadado,
}

fn comprueba_estado(mood: &Estado) -> i32 {
    use Estado::*; // Se importa el conjunto de variantes de Estado. Ahora se puede escribir menos
    let nivel_de_felicidad = match mood {
        Feliz => 10, // Ya no es necesario escribir Estado::
        Cansado => 6,
        NoEstoyMal => 7,
        Enfadado => 2,
    };
    nivel_de_felicidad
}

fn main() {
    let my_mood = Estado::NoEstoyMal;
    let nivel_de_felicidad = comprueba_estado(&my_mood);
    println!("De 1 a 10, mi estado de felicidad es {}", nivel_de_felicidad);
}

Las partes de un enumerado se pueden convertir a número entero. Esto se debe a que Rust da a cada variante de un enum un número que comienza con el 0 (para uso interno de Rust). Se puede utilizar en el código, siempre que las variantes con contengan ningún dato adicional:

enum Estacion {
    Primavera, // If this was Primavera(String) or something it wouldn't work
    Verano,
    Otoño,
    Invierno,
}

fn main() {
    use Estacion::*;
    let cuatro_estaciones = vec![Primavera, Verano, Otoño, Invierno];
    for estacion in cuatro_estaciones {
        println!("{}", estacion as u32);
    }
}

El código anterior imprime:

0
1
2
3

Es posible asignar un número entero expresamente a cada variante. A Rust no le importa el número concreto que tenga cada una de ellas. Para ello, se añade un símbolo = y el número deseado a cada variante. No es necesario indicar el número a cada variante. Si no se añade, Rust utiliza el siguiente disponible (suma 1) a partir de la variante anterior que tuviera número:

enum Estrella {
    EnanaMarron = 10,
    EnanaRoja = 50,
    EstrellaAmarilla = 100,
    GiganteRoja = 1000,
    EstrellaMuerta, // ¿Qué número tendrá?
}

fn main() {
    use Estrella::*;
    let starvec = vec![EnanaMarron, EnanaRoja, EstrellaAmarilla, GiganteRoja];
    for star in starvec {
        match star as u32 {
            size if size <= 80 => println!("No es la estrella más grande"),
            size if size >= 80 => println!("Esta estrella tiene un buen tamaño"),
            _ => println!("Esta estrella es muy grande"),
        }
    }
    println!("¿Qué número tiene EstrellaMuerta? Es el número {}.", EstrellaMuerta as u32);
}

This prints:

No es la estrella más grande
No es la estrella más grande
Esta estrella tiene un buen tamaño
Esta estrella tiene un buen tamaño
¿Qué número tiene EstrellaMuerta? Es el número 1001.

EstrellaMuerta hubiera sido el número 4 si no se hubiera expresado ningún número, pero ahora es el 1001.

Los enumerados sirven para usar tipos diferentes

Como ya se sabe, los elementos de un Vec, array, etc. tienen que ser del mismo tipo siempre (solo las tuplas permiten tipos diferentes). Los enumerados permiten incorporar diferentes tipos en las colecciones anteriores. Si se deseara tener un Vec que almacenara de forma indistinta u32 o i32 se puede declarar el Vec como que contiene un enumerado como en el siguiente ejemplo:

enum Numero {
    U32(u32),
    I32(i32),
}

fn main() {}

Así, este enumerado tiene dos variantes: la variante U32con un u32y la variante I32 con un i32. U32 y I32 son solo los nombres de cada variante. Se podrían haber llamado UTreintaYDos o ITreintaYDos o cualquier otra cosa.

Ahora es posible declarar un Vec de la siguiente forma Vec<Numero> y el compilador no se queja porque el vector es de un solo tipo. Al compilador no le preocupa si en un momento dado hay u32 o i32 porque esa diferencia está oculta por el tipo Numero. Y como es un enumerado, es necesario seleccionar una variante cada vez. En el siguiente código se usa el método .is_positive() para seleccionar la variante. Si es true se selecciona U32 y si es false se selecciona I32.

Ahora el código queda así:

enum Numero {
    U32(u32),
    I32(i32),
}

fn get_numero(input: i32) -> Numero {
    let numero = match input.is_positive() {
        true => Numero::U32(input as u32), // lo cambia a u32 si es positivo
        false => Numero::I32(input), 
    };
    numero
}


fn main() {
    let my_vec = vec![get_numero(-800), get_numero(8)];

    for item in my_vec {
        match item {
            Numero::U32(numero) => println!("Es un u32 con el valor {}", numero),
            Numero::I32(numero) => println!("Es un i32 con el valor {}", numero),
        }
    }
}

Este código imprime:

Es un i32 con el valor -800
Es un u32 con el valor 8