Lección 11 de 26 Structs, enums y patrones
Enums y pattern matching
- Un enum es un tipo con varias variantes. Un Semaforo es Rojo, Ambar o Verde: solo una a la vez.
- match compara el valor con cada patrón, de arriba abajo, y elige la rama del primero que encaja.
- La luz es Verde, así que el match devuelve 25: Ferris tiene 25 segundos para cruzar.
- match es exhaustivo: si olvidas una variante, como Ambar, el código no compila.
Un struct agrupa varios datos que van juntos: un usuario tiene nombre y email y edad. Pero muchas veces un valor es una cosa u otra: un semáforo está en rojo, en ámbar o en verde; un pago se hace en efectivo o con tarjeta. Para eso existen los enums (enumeraciones).
Los enums de Rust son mucho más potentes que los de otros lenguajes, porque cada variante puede llevar sus propios datos. Junto con match, que te permite decidir qué hacer en cada caso, son una de las herramientas que más vas a usar.
Definir un enum
Con enum declaras un tipo nuevo y enumeras sus posibles valores, llamados variantes:
enum Semaforo {
Rojo,
Ambar,
Verde,
}Para crear un valor escribes el nombre del enum, :: y la variante: let luz = Semaforo::Rojo;. Así, luz es de tipo Semaforo y solo puede valer una de sus tres variantes. Nunca «amarillo», nunca un número suelto: el compilador lo garantiza.
Variantes con datos
Cada variante puede guardar datos, y cada una los suyos. Hay tres formas de declararlas:
enum Mensaje {
Salir, // sin datos
Escribir(String), // con datos en forma de tupla
Mover { x: i32, y: i32 }, // con campos con nombre, como un struct
CambiarColor(u8, u8, u8), // una tupla con varios valores
}Se crean igual que una tuple struct o un struct normal: Mensaje::Escribir(String::from("hola")) o Mensaje::Mover { x: 3, y: -1 }.
Podrías modelarlo con cuatro structs distintos, pero entonces no podrías escribir una función que reciba «un mensaje cualquiera». Con el enum, todas las variantes son del mismo tipo: Mensaje.
match: decide según la variante
match compara un valor con una lista de patrones y ejecuta la rama del primero que encaja:
enum Semaforo {
Rojo,
Ambar,
Verde,
}
fn segundos(luz: Semaforo) -> u32 {
match luz {
Semaforo::Rojo => 30,
Semaforo::Ambar => 5,
Semaforo::Verde => {
println!("¡Adelante!");
25
}
}
}
fn main() {
println!("Rojo: {} s", segundos(Semaforo::Rojo));
println!("Ámbar: {} s", segundos(Semaforo::Ambar));
println!("Verde: {} s", segundos(Semaforo::Verde));
}- Cada rama tiene la forma
patrón => expresióny se separa de la siguiente con una coma. matches una expresión: devuelve el valor de la rama elegida. Aquí es el valor de retorno de la función.- Si una rama necesita varias líneas, usa un bloque
{ }. Su última expresión (sin;) es el valor de la rama.
match es exhaustivo
match te obliga a cubrir todos los casos. Si te olvidas de una variante, el programa no compila:
enum Semaforo { Rojo, Ambar, Verde }
fn segundos(luz: Semaforo) -> u32 {
match luz {
Semaforo::Rojo => 30,
Semaforo::Verde => 25,
}
}
fn main() {
println!("{}", segundos(Semaforo::Ambar));
}El compilador te dice exactamente qué falta: pattern `Semaforo::Ambar` not covered. Es una de las grandes ventajas de Rust: si mañana añades una variante Intermitente, el compilador te señalará cada match que tienes que actualizar.
Patrones que extraen datos
Lo mejor de match es que, además de elegir la rama, saca los datos de la variante y los guarda en variables:
enum Mensaje {
Salir,
Escribir(String),
Mover { x: i32, y: i32 },
CambiarColor(u8, u8, u8),
}
fn procesar(msg: Mensaje) {
match msg {
Mensaje::Salir => println!("Adiós"),
Mensaje::Escribir(texto) => println!("Texto: {texto}"),
Mensaje::Mover { x, y } => println!("Mover a ({x}, {y})"),
Mensaje::CambiarColor(r, g, b) => println!("Color RGB({r}, {g}, {b})"),
}
}
fn main() {
procesar(Mensaje::Escribir(String::from("hola")));
procesar(Mensaje::Mover { x: 3, y: -1 });
procesar(Mensaje::CambiarColor(255, 128, 0));
procesar(Mensaje::Salir);
}En Mensaje::Escribir(texto), la variable texto recibe el String que va dentro. En Mensaje::Mover { x, y } los campos se extraen por su nombre, igual que al desestructurar un struct.
Métodos en enums
Como con los structs, puedes añadir métodos a un enum con impl. Dentro, lo normal es hacer match self:
enum Forma {
Cuadrado(f64),
Rectangulo { ancho: f64, alto: f64 },
}
impl Forma {
fn area(&self) -> f64 {
match self {
Forma::Cuadrado(lado) => lado * lado,
Forma::Rectangulo { ancho, alto } => ancho * alto,
}
}
}
fn main() {
let formas = [Forma::Cuadrado(3.0), Forma::Rectangulo { ancho: 2.0, alto: 5.0 }];
for forma in formas {
println!("Área: {}", forma.area());
}
}Como self es una referencia, lado, ancho y alto también lo son (&f64). No tienes que hacer nada especial: Rust sabe multiplicar referencias a números. Fíjate también en el array: guarda un cuadrado y un rectángulo a la vez, porque los dos son del tipo Forma.
Más patrones: varios valores, rangos y guardas
match no es solo para enums. Funciona con números, caracteres, booleanos… y tiene varios trucos:
fn main() {
for n in [-4, 0, 2, 7, 15] {
let texto = match n {
0 => "cero",
1 | 2 | 3 => "pocos",
4..=9 => "unos cuantos",
x if x < 0 => "negativo",
_ => "muchos",
};
println!("{n}: {texto}");
}
}1 | 2 | 3encaja con cualquiera de esos valores:|significa «o».4..=9encaja con un rango inclusivo, del 4 al 9 ambos incluidos. Funciona con números y conchar('a'..='z').x if x < 0es una guarda: el patrónxacepta cualquier número y lo guarda enx, pero la rama solo se elige si además se cumple la condición._es el comodín: encaja con todo. Va al final para recoger «el resto de casos».
Orden: Las ramas se comprueban de arriba abajo y gana la primera que encaja. Si pones
_antes que otras ramas, esas ramas nunca se ejecutarán y el compilador te avisará con ununreachable pattern.
El comodín también sirve para ignorar partes de un patrón: Mensaje::CambiarColor(r, _, _) extrae solo el rojo, y Mensaje::Mover { x, .. } solo el campo x.
if let: cuando solo te interesa un caso
A veces solo quieres hacer algo si el valor es una variante concreta. Con match tendrías que añadir una rama _ => {} vacía. if let es más corto:
enum Comando {
Saltar,
Decir(String),
}
fn main() {
let comandos = [Comando::Decir(String::from("¡Hola!")), Comando::Saltar];
for comando in comandos {
if let Comando::Decir(texto) = comando {
println!("Dice: {texto}");
} else {
println!("Otro comando");
}
}
}if let PATRÓN = VALOR ejecuta el bloque solo si el valor encaja con el patrón y, de paso, extrae sus datos. El else es opcional.
let … else: extrae o sal
let … else funciona al revés: si el valor encaja, extrae los datos y sigue; si no, ejecuta el bloque else, que está obligado a salir (con return, break, continue o un panic!):
enum Pedido {
Pendiente,
Enviado { codigo: u32 },
}
fn seguimiento(pedido: Pedido) {
let Pedido::Enviado { codigo } = pedido else {
println!("Todavía no se ha enviado");
return;
};
println!("Código de seguimiento: {codigo}");
}
fn main() {
seguimiento(Pedido::Enviado { codigo: 4521 });
seguimiento(Pedido::Pendiente);
}Así no tienes que meter el resto de la función dentro de un if let: los casos que no te sirven se descartan al principio y después trabajas con el dato ya extraído.
Resumen: Usa
matchcuando quieras tratar todos los casos,if letcuando solo te importe uno ylet … elsecuando sin ese dato no tenga sentido continuar.
Hora de practicar
Ejercicios
0/4 resueltos
Reto 1 de 4 Resuelto
match de la función valor para que devuelva cuántos céntimos vale cada moneda: UnCentimo vale 1, DiezCentimos 10, UnEuro 100 y DosEuros 200. Ahora mismo no compila porque faltan variantes. No uses el comodín _.Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.