Enums en Rust – Aprende Rust 10/x

enums en rust

En este post veremos las enumeraciones (enumerations), o enums en Rust. Estos nos permiten crear un tipo personalizado, enumerando los posibles valores que pueda tener. Los enums traen otro par de temas interesantes: el tipo de enum llamado Option y algo que ya vimos en el ejemplo de adivinar el número, que se llama pattern matching.

En los structs vimos que nos servían para agrupar datos de una manera lógica, como podrían ser el teléfono y un email dentro de un modelo de Cliente, o como lo vimos con nuestro struct Rectangle con sus propiedades height y width. En los enums, podríamos definir, entonces si los posibles valores para una variable podrían ser Cliente o Proveedor o en el otro ejemplo un Circle o un Rectangle.

Definición de enums en Rust

Hay varias maneras de declarar los enum en Rust:

Simple

Vamos a declarar un enum para los colores:

enum MiColorRGB {
    Rojo,
    Verde,
    Azul,
}

let semaforo = MiColorRGB::Rojo;

Como vemos, no tenemos un tipo asignado (i32 o String), solamente lo usamos para lo que es, es decir, lo usaremos para saber el tipo de acción o dato que estamos asignando.

NOTA: aquí, el compilador de Rust nos lanzaría un error porque solo hemos usado Rojo y no los otros. Es importante tenerlo en cuenta al momento de declarar nuestros enums.

Múltiples tipos de datos

También podemos declarar un enum en el que cada miembro pueda recibir un valor, por ejemplo:

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

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

Aquí declaramos el enum llamado IpAddr donde la versión 4 de la IP recibe los 4 valores correspondientes a cada octeto, mientras que la versión 6 recibe una cadena con su valor.

Datos asociados

Igualmente podemos declarar cada elemento de nuestro enum con la notación de los structs.

enum Persona {
    Child { edad: u8 },
    Adult { edad: u8 },
}

let persona = Persona::Adult{ edad: 28 };

Uso con structs

También podemos usar nuestros structs como tipo de dato para nuestros miembros de los enums:

struct Ipv4Addr {
    oct1: u8,
    oct2: u8,
    oct3: u8,
    oct4: u8,
}

struct Ipv6Addr {
    addr: String,
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

Implementación (impl)

Hay algo parecido entre los enums y los structs: el hecho de definir métodos dentro. Aunque suene un poco raro, es posible llamar a un método para una “instancia” de un enum:

fn main() {
    enum Animal {
        Perro,
    }

    impl Animal {
        fn sonido(&self, sonido: String) {
            println!("{}", sonido);
        }
    }

    let m = Animal::Perro;
    m.sonido(String::from("guau guau!"));
}

Usamos la palabra clave impl, luego el nombre de nuestro enum, en este caso Animal. Luego hacemos la declaración normal, usando la palabra fn después el nombre del método. Aquí vemos que está la palabra &self. Esta es una constante y siempre debe usarse como el primer parámetro. Después de esto vienen los demás parámetros.

Para usarlo, instanciamos nuestro enum, eligiendo Perro como opción y luego llamamos al método que creamos.

Pattern matching en Rust

En nuestro ejemplo anterior, instanciamos un Perro y luego mandamos su sonido. En realidad, al instanciar Perro, deberíamos saber ya su sonido.

Para resolver esto, Rust tiene un control de flujo muy bueno que usa la palabra match para identificar los valores que quieres validar. Es como un tipo de switch en otros lenguajes. Esto se llama “pattern matching” y la posibles opciones a evaluar se llaman “arms” (brazos):

fn main() {
    enum Animal {
        Perro,
        Gato,
    }

    impl Animal {
        fn sonido(&self) {
            match self {
                Animal::Perro => println!("Guau!"),
                Animal::Gato => {
                    println!("Miau!")
                }
            }
        }
    }

    let mut m = Animal::Perro;
    m.sonido();

    m = Animal::Gato;
    m.sonido();
}

En este ejemplo, vemos que match evalúa el parámetro self y dentro de las llaves están las dos opciones que tenemos declaradas en el enum principal.

También podemos crearnos una función que reciba nuestro Animal y que esa misma función sea la que evalúe:

enum Animal {
    Perro,
    Gato,
}

fn main() {
    impl Animal {
        fn sonido(&self) {
            match self {
                Animal::Perro => println!("Guau!"),
                Animal::Gato => {
                    println!("Miau!")
                }
            }
        }
    }

    let mut m = Animal::Perro;
    m.sonido();

    m = Animal::Gato;
    m.sonido();

    let animalito = que_animal_soy(m);
    println!("Animal: {}", animalito);
}

fn que_animal_soy(animal: Animal) -> String {
    match animal {
        Animal::Perro => String::from("Soy un perrito"),
        Animal::Gato => String::from("Soy un gatito"),
    }
}

¿Puedo agregar un “default” tal como en switch? De poder, se puede. Desde la versión 1.61 de Rust es posible hacerlo, pero no lo tendremos en cuenta. Por ahora diremos que si Rust no ve que evaluamos todas las posibles opciones que hemos declarado en nuestro enum entonces no podremos compilar.

Es cierto que podemos declarar el enum algo así:

enum Animal {
    Perro,
    Gato,
    Otro(String),
}

let animal = Animal::Otro(String::from("Tigre"));

Y con ello podremos crearnos el enum con otro animal, pero no es la idea al usar este tipo de dato.

Option en Rust

Sin embargo, el no tener una opción “default” puede ser algo necesario algunas veces. Para ello Rust tiene el operador Option.

Option es un enum genérico que se usa para representar un valor que no existe, su sintaxis es la siguiente:

enum Option<T> {
    Some(T),
    None,
}
  • Some(T): representa el valor que sí existe de tipo T. En nuestro ejemplo sería Perro o Gato.
  • None: representa precisamente algo que no existe.
#[derive(Debug)]
enum Animal {
    Perro,
    Gato,
}

fn main() {
    impl Animal {
        fn sonido(&self) {
            match self {
                Animal::Perro => println!("Guau!"),
                Animal::Gato => {
                    println!("Miau!")
                }
            }
        }
    }

    let mut m = Animal::Perro;
    m.sonido();

    m = Animal::Gato;
    m.sonido();

    let animalito = que_animal_soy(m);
    println!("Animal: {}", animalito);

    let mi_animal = adivina_animal(String::from("Pez"));
    match mi_animal {
        Some(val) => println!("{:?}", val),
        None => println!("Sin valor"),
    }
}

fn que_animal_soy(animal: Animal) -> String {
    match animal {
        Animal::Perro => String::from("Soy un perrito"),
        Animal::Gato => String::from("Soy un gatito"),
    }
}

fn adivina_animal(animal: String) -> Option<Animal> {
    if animal == "Perro" {
        Some(Animal::Perro)
    } else if animal == "Gato" {
        Some(Animal::Gato)
    } else {
        None
    }
}

Creamos la función adivina_animal que nos regresará un enum Option de tipo Animal. La función evaluará con puros if si el string que enviamos es igual a Perro o Gato y devolverá Some, el cual recibe el tipo de animal que hayamos enviado a la función. De lo contrario lanzará la opción None.

Al usar match debemos cubrir todas las posibilidades, es por eso que debemos incluir también la opción None cuando usemos el tipo Option, esa regla no se rompe en ningún lugar.

Creo que con esto nos damos una idea del uso de enums en Rust y varias opciones para usarlo de acuerdo a nuestro programa.

Más info acá.

Gracias por leer.


Posted

in

, , ,

by