Ir al contenido

Enums en Rust

Un enum (enumeración) define un tipo que puede ser una opción entre un conjunto fijo de posibilidades. Si un struct sirve para decir “esta cosa tiene todos estos campos”, un enum sirve para decir “esta cosa es exactamente una de estas opciones”.

enum Direction {
North,
South,
East,
West,
}
fn main() {
let heading = Direction::North;
match heading {
Direction::North => println!("Rumbo al norte"),
Direction::South => println!("Rumbo al sur"),
Direction::East => println!("Rumbo al este"),
Direction::West => println!("Rumbo al oeste"),
}
}

Salida:

Rumbo al norte
▶ Pruébalo tú mismo

Fíjate en que este match no necesita un comodín _: como aparecen todas las variantes posibles de Direction, Rust ya sabe que el match está completo.

Aquí es donde los enums de Rust van más allá que los de otros lenguajes: cada variante puede llevar sus propios datos:

enum Shape {
Circle(f64), // radio
Rectangle(f64, f64), // ancho, alto
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 3.14159 * radius * radius,
Shape::Rectangle(width, height) => width * height,
}
}
fn main() {
let circle = Shape::Circle(3.0);
let rectangle = Shape::Rectangle(4.0, 5.0);
println!("{:.2}", area(&circle));
println!("{:.2}", area(&rectangle));
}

Salida:

28.27
20.00
▶ Pruébalo tú mismo

Shape::Circle lleva un f64 (el radio); Shape::Rectangle lleva dos. En cada brazo, match saca esos datos directamente: no hace falta ningún paso extra para “desenvolverlos”.

Igual que un struct, un enum puede tener un bloque impl con métodos. Dentro de un método, lo normal es hacer match self para decidir qué hacer con cada variante:

#[derive(Debug)]
enum Light {
Red,
Yellow,
Green,
}
impl Light {
fn seconds(&self) -> u32 {
match self {
Light::Red => 30,
Light::Yellow => 3,
Light::Green => 25,
}
}
fn next(&self) -> Light {
match self {
Light::Red => Light::Green,
Light::Green => Light::Yellow,
Light::Yellow => Light::Red,
}
}
}
fn main() {
let mut light = Light::Red;
for _ in 0..4 {
println!("{:?} durante {} segundos", light, light.seconds());
light = light.next();
}
}

Salida:

Red durante 30 segundos
Green durante 25 segundos
Yellow durante 3 segundos
Red durante 30 segundos
▶ Pruébalo tú mismo

next devuelve un Light totalmente nuevo, y light = light.next(); reemplaza el anterior. Por eso light se declara con mut. #[derive(Debug)] funciona con los enums igual que con los structs, así que {:?} imprime el nombre de la variante (por eso sale Red y no “rojo”: es el nombre que le dimos en el código).

Poner la lógica en métodos la mantiene junto al tipo. Cualquiera que use un Light puede llamar a light.seconds() sin saber cómo se decide la duración. Y si más adelante añades una variante nueva, como FlashingYellow, el compilador te señala cada match que tienes que actualizar.

El tipo Option, que verás con calma en su propia página, no es más que un enum definido en la biblioteca estándar:

enum Option<T> {
Some(T),
None,
}

Cuando entiendes los enums, Option deja de parecer una sintaxis especial: es un enum totalmente normal con dos variantes, y una de ellas lleva un valor.

¿Por qué no usar un struct con un campo “tipo”?

Sección titulada «¿Por qué no usar un struct con un campo “tipo”?»

Podrías intentar imitar esto con un struct y un campo de texto como "circle" o "rectangle". Pero entonces nada te impide escribir "circl" por error, y tendrías que comprobar a mano qué campos tienen sentido para cada tipo. Con un enum, las combinaciones inválidas directamente no se pueden escribir, y el compilador te obliga a tratar todas las variantes.

Crea un enum Coin con las variantes Penny, Nickel, Dime y Quarter (monedas de 1, 5, 10 y 25 centavos). Escribe fn value_in_cents(coin: &Coin) -> u32 y luego suma un monedero con estas monedas: una de 25, una de 10, una de 5, una de 1 y otra de 25. Pista: guarda las monedas en un array y recórrelo con &purse.

Ver solución
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
let purse = [Coin::Quarter, Coin::Dime, Coin::Nickel, Coin::Penny, Coin::Quarter];
let mut total = 0;
for coin in &purse {
total += value_in_cents(coin);
}
println!("Total: {total} centavos");
}

Salida:

Total: 66 centavos
▶ Pruébalo tú mismo

Toma el enum Shape de esta página y añádele una variante Triangle que guarde las longitudes de sus tres lados. Después escribe una función perimeter (la distancia alrededor de la figura) que funcione con todas las formas. Pista: el perímetro de un círculo es 2.0 * 3.14159 * radius. El de un triángulo es la suma de sus tres lados.

Ver solución
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn perimeter(shape: &Shape) -> f64 {
match shape {
Shape::Circle(radius) => 2.0 * 3.14159 * radius,
Shape::Rectangle(width, height) => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
}
}
fn main() {
let shapes = [
Shape::Circle(1.0),
Shape::Rectangle(4.0, 5.0),
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:.2}", perimeter(shape));
}
}

Salida:

6.28
18.00
12.00
▶ Pruébalo tú mismo

Prueba a borrar el brazo Shape::Triangle del match. Rust se niega a compilar, porque el match ya no cubre todas las variantes. Así se asegura el compilador de que nunca te olvides de un caso.

Crea un enum Temperature con dos variantes que guarden cada una un f64: Celsius y Fahrenheit. Añade un método to_celsius(&self) -> f64 e imprime Celsius(25.0), Fahrenheit(212.0) y Fahrenheit(50.0) en °C con un decimal. Pista: para convertir, usa (f - 32.0) * 5.0 / 9.0.

Ver solución
enum Temperature {
Celsius(f64),
Fahrenheit(f64),
}
impl Temperature {
fn to_celsius(&self) -> f64 {
match self {
Temperature::Celsius(c) => *c,
Temperature::Fahrenheit(f) => (f - 32.0) * 5.0 / 9.0,
}
}
}
fn main() {
let readings = [
Temperature::Celsius(25.0),
Temperature::Fahrenheit(212.0),
Temperature::Fahrenheit(50.0),
];
for t in &readings {
println!("{:.1} °C", t.to_celsius());
}
}

Salida:

25.0 °C
100.0 °C
10.0 °C
▶ Pruébalo tú mismo

Como self está prestado, c y f son referencias (&f64). Las operaciones matemáticas funcionan directamente con referencias, pero para devolver el valor en Celsius tal cual escribes *c, que significa “el número al que apunta c”.

  • Un enum define un tipo que es una de varias variantes con nombre: enum Direction { North, South, ... }.
  • Las variantes pueden llevar sus propios datos, y cada variante puede llevar datos distintos.
  • match extrae los datos de un enum de forma natural y te obliga a tratar todas las variantes.
  • Los enums pueden tener métodos en un bloque impl, igual que los structs.
  • Option (y Result, que veremos más adelante) son simplemente enums de la biblioteca estándar; no tienen nada de mágico.

Comprueba lo aprendido

1. ¿Qué puede hacer cada variante de un enum de Rust, a diferencia de los enums típicos de otros lenguajes?

2. ¿Por qué el match del ejemplo de Direction no necesita un brazo comodín _?

3. ¿Cómo está implementado realmente Option<T> en la biblioteca estándar?

Puntuación: 0 / 3