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.
Enums en Rust
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 tipoT
. En nuestro ejemplo seríaPerro
oGato
.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.
Gracias por leer.