Enums con y sin datos: la unión etiquetada segura
El tipo suma de Rust: un valor que es exactamente una de varias variantes, y donde cada variante puede llevar su propia carga de datos. De los enums-nombre de C a la unión etiquetada que el compilador vigila por ti.
Un struct agrupa datos que van juntos: un punto tiene una x y una y. Un enum modela lo contrario: un valor que es una de varias cosas posibles, nunca dos a la vez. El salto que separa a Rust de casi todos los lenguajes de sistemas es que cada variante puede llevar su propia carga de datos, con forma distinta, mientras el compilador garantiza que jamás leerás la carga equivocada.
- Entender el
enumcomo tipo suma: un valor que es exactamente una variante. - Adjuntar datos a las variantes en sus tres formas: unidad, tupla y struct.
- Modelar dominios reales con
MessageyShape. - Ver por dentro la etiqueta más carga y por qué es una unión segura.
De un nombre a una posibilidad con datos
En C, un enum es azúcar sobre enteros: un puñado de nombres para constantes. En Rust ese caso existe, pero es el más pobre de una idea mucho mayor. La variante puede llevar datos, y cada una puede llevarlos con una forma distinta:
enum Message { Quit, // variante unidad: no lleva datos Move { x: i32, y: i32 }, // variante struct: campos con nombre Write(String), // variante tupla: un dato posicional ChangeColor(u8, u8, u8), // variante tupla: tres datos}Un valor de tipo Message es exactamente uno de esos cuatro casos: o es Quit, o es un Move con sus coordenadas, o un Write con su texto, o un ChangeColor con su terna. Nunca dos a la vez, nunca ninguno. Esto es lo que en teoría de tipos se llama un tipo suma: el número de valores posibles del enum es la suma de los valores de sus variantes, igual que un struct es un tipo producto (su cardinalidad es el producto de la de sus campos). De ahí el nombre tipos algebraicos de datos, y de ahí que un enum y un struct sean las dos mitades duales del modelado de datos.
Las tres formas de una variante
Cada variante elige su forma según lo que necesite representar, y puedes mezclarlas en el mismo enum:
- Unidad —
Quit. No transporta nada; solo importa que sea ese caso. - Tupla —
Write(String),ChangeColor(u8, u8, u8). Datos posicionales, como una tupla anónima pegada a la variante. - Struct —
Move { x: i32, y: i32 }. Campos con nombre, cuando la claridad importa.
Un enum bien elegido hace que su impl sea limpio. Modelemos figuras geométricas y su área, donde cada forma necesita distintos datos:
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Triangle { base: f64, height: f64 },}impl Shape { fn area(&self) -> f64 { match self { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Triangle { base, height } => 0.5 * base * height, } }}Fíjate en lo que no aparece: ni un radius colgando en un Rectangle, ni un campo kind que haya que mantener a mano, ni un if comprobando qué es esto. Los datos de cada figura viven dentro de su variante y solo son accesibles cuando has confirmado la variante. La x de un Move sencillamente no existe fuera de un Move.
Un enum es un tipo, sus variantes no
Shape es un tipo; Shape::Circle es un constructor de ese tipo, no un tipo por sí mismo. No puedes declarar una función que reciba Shape::Circle: recibe Shape y desambigua dentro con match. Esta es una diferencia deliberada con las jerarquías de clases, donde cada subclase sí es un tipo. Rust lo cierra a propósito, y en la lección 6.5 verás por qué esa clausura es una fortaleza y no una carencia.
Por dentro: etiqueta más carga
En memoria, un enum es una unión etiquetada: un discriminante (la etiqueta, que dice qué variante es) seguido de espacio suficiente para la carga de la variante más grande. El tamaño total es el de la variante mayor más el discriminante, con el ajuste de alineación:
use std::mem::size_of;enum Small { A, B, C } // solo etiqueta: 1 byteenum Payload { Nothing, Num(u64) } // etiqueta + hueco para u64fn main() { assert_eq!(size_of::<Small>(), 1); // el u64 exige alineacion de 8, asi que el total sube a 16 assert_eq!(size_of::<Payload>(), 16);}Compáralo con la union de C, que superpone sus campos sin etiqueta: el programador debe recordar por su cuenta cuál campo es válido, y leer el equivocado es comportamiento indefinido. El enum de Rust carga la etiqueta automáticamente y match la comprueba; por eso es una unión segura. Pagas un discriminante a cambio de que corromper la memoria por confundir variantes sea, literalmente, imposible.
Y cuando ese discriminante es innecesario, el compilador lo elimina con la optimización de nicho: si una variante ya tiene un patrón de bits imposible, ese hueco codifica otra variante gratis. El caso emblemático es Option:
use std::mem::size_of;fn main() { assert_eq!(size_of::<Option<u64>>(), 16); // sin nicho: 8 de dato + etiqueta assert_eq!(size_of::<Option<&u64>>(), 8); // una referencia nunca es null: null = None assert_eq!(size_of::<Option<Box<u64>>>(), 8); // igual: el puntero no nulo deja hueco}Option<&u64> ocupa lo mismo que un puntero desnudo porque el patrón nulo, que una & jamás toma, se reserva para None. Es la seguridad de Option sin coste de tamaño ni de velocidad: el famoso zero-cost abstraction aplicado al tipo que sustituye al null.
Discriminantes explícitos y representación
Cuando el enum no lleva datos, puedes fijar el valor entero de cada variante, imprescindible para protocolos binarios y para hablar con C:
#[repr(u8)]enum Estado { Inactivo = 0, Activo = 1, Error = 255,}fn main() { let code = Estado::Error as u8; // 255: casteo directo a entero assert_eq!(code, 255);}El atributo #[repr(u8)] fija el tipo y el tamaño del discriminante, algo que necesitas para serializar un byte en la red o encajar en una estructura de C. Sin datos, un enum es intercambiable con un entero acotado; con datos, #[repr] controla además cómo se dispone la etiqueta frente a la carga, dándote una representación en memoria exacta sin renunciar a la seguridad del match en el lado de Rust.
Un enum sin datos no es un int con nombres
Aunque Estado compile a un u8, tratarlo como enum y no como entero desnudo te regala la exhaustividad del match y la imposibilidad de construir un valor fuera de rango. Un u8 admite 256 valores; Estado admite tres, y el compilador lo sabe y lo aprovecha. Nunca tendrás un Estado con el valor 7, mientras que un u8 = 7 disfrazado de estado es el bug clásico de C que este tipo vuelve imposible.
El enum es la mitad del modelado que otros lenguajes te niegan
Interioriza esta simetría: struct es y, enum es o. Casi todos los lenguajes de sistemas te dan el y de primera clase (structs, clases, tuplas) pero te dejan huérfano en el o: para expresar “esto es A o B o C” acabas con una etiqueta manual y una union, o con jerarquías de herencia, o con punteros que podrían ser null. Todas esas soluciones comparten un defecto: el compilador no sabe que son excluyentes, así que no puede protegerte. Rust eleva el o al mismo rango que el y, y de esa decisión cuelga media identidad del lenguaje. Option deja de ser un puntero que reza por no ser null y pasa a ser “hay valor o no lo hay”, verificado. Result es “éxito o error”, verificado. Un AST es “esto es o un literal o una suma o una llamada”, verificado. Cuando piensas en o con la misma naturalidad con la que piensas en y, empiezas a diseñar tipos cuya forma calca la forma de la realidad, y los estados imposibles dejan de poder escribirse. Eso no es una característica del lenguaje: es una manera distinta de pensar los datos.
Diseña un enum que calque un dominio
- Escribe el
enum Messagede arriba y una funciónfn procesa(m: Message)que imprima algo distinto para cada variante extrayendo sus datos. - Añade a
Shapeuna varianteSquare { side: f64 }y comprueba qué te obliga a cambiar el compilador (te lo diráarea). - Con
std::mem::size_ofmideShapey explica por qué vale lo que vale a partir de su variante mayor. - Compara
size_of::<Option<u32>>()consize_of::<Option<std::num::NonZeroU32>>()y razona qué nicho aprovecha el segundo. - Escribe en un comentario por qué una
unionde C no podría dar la misma garantía que esteenum.