Un enum en Rust es un tipo que puede representar una de varias variantes posibles.
Si vienes de C, C# o Java, probablemente piensas en una enumeración como una forma bonita de asignar nombres a números enteros.
Color.Redes0.Color.Greenes1.
Es útil para máquinas de estado o configuraciones, pero poco más.
En Rust, los Enums son una bestia completamente diferente. Rust adopta el concepto de Tipos de Datos Algebraicos (ADTs) de los lenguajes funcionales (como Haskell).
Esto significa que un enum en Rust no solo nos dice “qué variante es”, sino que cada variante puede contener datos diferentes en su interior.
Enums simples
Empecemos por lo básico, lo que ya conoces. Un Enum nos permite definir un tipo que puede ser una de varias variantes posibles.
enum Sentido {
Norte,
Sur,
Este,
Oeste,
}
fn main() {
let direccion = Sentido::Norte;
}Hasta aquí, nada nuevo. Podemos usarlo para dar claridad al código en lugar de usar números mágicos (0, 1, 2, 3).
Enums con datos
Aquí viene lo interesante. En Rust, no necesitamos crear un struct separado para asociar datos a una variante. Podemos guardar los datos directamente dentro de la variante del enum.
Imagina que queremos modelar direcciones IP. Tenemos dos estándares: V4 y V6.
- Una dirección V4 son cuatro números de 8 bits (ej: 127.0.0.1).
- Una dirección V6 es un String largo.
En C# o Java, tendríamos que crear una clase base IpAddr y dos subclases, o un struct con campos opcionales. En Rust, lo modelamos así:
enum IpAddr {
V4(u8, u8, u8, u8), // Esta variante guarda 4 bytes
V6(String), // Esta variante guarda un String
}
fn main() {
let casa = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}Fíjate en lo potente que es esto:
IpAddr::V4yIpAddr::V6son del mismo tipo (IpAddr). Podemos pasar cualquiera de los dos a una función que espere una IP.- Cada variante guarda tipos de datos completamente distintos.
Un sistema de mensajes
Para ver la flexibilidad real, mira este ejemplo de un enum que define los mensajes que se pueden enviar en una aplicación:
enum Mensaje {
Salir, // No tiene datos asociados (como un enum de C)
Mover { x: i32, y: i32 }, // Tiene campos con nombre (como un Struct)
Escribir(String), // Tiene un solo String (como una Tuple Struct)
CambiarColor(i32, i32, i32), // Tiene tres enteros (como una Tupla)
}Si quisiéramos modelarlo solo con structs, necesitaríamos cuatro tipos distintos y algún mecanismo adicional para tratarlos como “cualquier mensaje”. Con el enum, todo queda agrupado bajo el tipo Mensaje.
Métodos en enums
Al igual que los Structs, los Enums también pueden tener bloques impl para definir métodos.
impl Mensaje {
fn ejecutar(&self) {
// Aquí iría la lógica (usando match, que veremos en el siguiente artículo)
}
}
fn main() {
let m = Mensaje::Escribir(String::from("Hola"));
m.ejecutar();
}El enum Option
En la mayoría de lenguajes existe el concepto de null (o nil, None). Es el “valor de los mil millones de dólares” (el error más costoso de la historia), porque intentar usar un valor nulo hace que el programa explote (NullReferenceException).
Los tipos y referencias ordinarios de Rust no usan null como valor válido. Los punteros crudos sí pueden ser nulos, sobre todo al interoperar con otros lenguajes, pero quedan fuera de las garantías habituales de las referencias.
¿Cómo expresamos entonces que algo puede no existir? (Por ejemplo, buscar un usuario en una base de datos y no encontrarlo).
Rust utiliza un Enum genérico incluido en la librería estándar llamado Option.
enum Option<T> {
None, // Representa la ausencia de valor (como null)
Some(T), // Representa que SÍ hay un valor de tipo T
}Este enum es tan importante que Rust lo importa automáticamente (no hace falta escribir Option::Some, basta con Some).
¿Por qué Option es mejor que null?
Imagina que tienes un número i32. En Rust, un i32 SIEMPRE es un número válido. Nunca puede ser null. El compilador te garantiza que puedes hacer matemáticas con él de forma segura.
Si quieres un número que podría no existir, debes usar Option<i32>.
let numero: i32 = 5;
let numero_opcional: Option<i32> = Some(5);
let numero_vacio: Option<i32> = None;
// let suma = numero + numero_opcional; // ❌ ¡ERROR!El compilador no te deja sumar un i32 con un Option<i32>.
¿Por qué? Porque numero_opcional podría ser None. Rust te obliga a comprobar antes de usarlo si hay un valor o no.
Esto obliga a representar y tratar la ausencia de forma explícita. Aún podemos provocar un panic si usamos unwrap() sobre None, pero no podemos confundir sin más un Option<T> con un T.