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”.
Definir y usar un enum
Sección titulada «Definir y usar un enum»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 norteFí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.
Los enums pueden guardar datos
Sección titulada «Los enums pueden guardar datos»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.2720.00Shape::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”.
Los enums también pueden tener métodos
Sección titulada «Los enums también pueden tener métodos»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 segundosGreen durante 25 segundosYellow durante 3 segundosRed durante 30 segundosnext 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.
Option: un enum que ya has estado usando
Sección titulada «Option: un enum que ya has estado usando»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.
Práctica
Sección titulada «Práctica»1. Cuenta las monedas
Sección titulada «1. Cuenta las monedas»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 centavos2. Añade un triángulo
Sección titulada «2. Añade un triángulo»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.2818.0012.00Prueba 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.
3. Temperaturas
Sección titulada «3. Temperaturas»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 °C100.0 °C10.0 °CComo 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”.
Resumen
Sección titulada «Resumen»- 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.
matchextrae 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(yResult, que veremos más adelante) son simplemente enums de la biblioteca estándar; no tienen nada de mágico.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»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
Para leer más
Sección titulada «Para leer más»- Defining an Enum — The Rust Book (en inglés): variantes con datos, métodos en enums y por qué Rust tiene
Optionen lugar de null. - The
matchControl Flow Construct — The Rust Book (en inglés):matchcon enums a fondo, con un ejemplo que clasifica monedas y patrones comodín. - Enums — Rust by Example (en inglés): ejemplos ejecutables, incluidas variantes con campos con nombre como
Click { x: i64, y: i64 }. - Enumerations — The Rust Reference (en inglés): todas las reglas, como dar a las variantes valores numéricos explícitos.
Option— documentación de la biblioteca estándar (en inglés): la definición real del enum que más vas a usar, con todos sus métodos auxiliares.