zig-enums-unions

Enums y uniones etiquetadas en Zig

  • 5 min

Un enum en Zig es un tipo que representa una opción dentro de un conjunto cerrado de valores. Hasta ahora, nuestros structs han sido rígidos, pero en el mundo real muchas veces necesitamos modelar estados o datos mutuamente excluyentes.

Un resultado de una operación web puede ser Éxito (con datos JSON) O un Error (con un código HTTP). Nunca ambos a la vez.

En Zig podemos modelarlo mediante enums y uniones etiquetadas, sin recurrir a una jerarquía de clases.

Enums: más que números

Un enum (enumeración) define un conjunto cerrado de valores posibles. A nivel de máquina, suelen ser simples enteros, pero Zig nos permite tratarlos con una semántica mucho más rica.

const Estado = enum {
    inactivo,
    cargando,
    listo,
    fallo,
};

const estado_actual = Estado.cargando;
Copied!

Tipos subyacentes

Por defecto, Zig decide qué entero usar para representar cada caso. Pero si necesitamos interactuar con hardware o protocolos binarios, podemos elegir el tipo exacto (u8, u32, etc.) y los valores.

// Forzamos a que sea un u8 y definimos valores específicos
const CodigoHTTP = enum(u16) {
    ok = 200,
    not_found = 404,
    server_error = 500,
};
Copied!

Enums con métodos

Aquí Zig se separa de C. Como un enum también es un contenedor, puede tener funciones y métodos.

Esto permite mantener la lógica relacionada junto al tipo. En lugar de tener una función esEstadoFinal(estado), podemos definir un método sobre el propio estado.

const EstadoJuego = enum {
    menu,
    jugando,
    game_over,

    // Método que recibe 'self'
    pub fn sePuedePausar(self: EstadoJuego) bool {
        // 'self' es el valor actual del enum
        return self == .jugando;
    }
    
    // Función de transición
    pub fn siguiente(self: EstadoJuego) EstadoJuego {
        return switch (self) {
            .menu => .jugando,
            .jugando => .game_over,
            .game_over => .menu,
        };
    }
};

pub fn main() void {
    const st = EstadoJuego.jugando;
    if (st.sePuedePausar()) {
        std.debug.print("Pausa permitida\n", .{});
    }
}
Copied!

Esta característica permite agrupar datos y comportamiento relacionado sin introducir clases.

Uniones sin etiqueta

Una union es un tipo de dato que puede almacenar uno de varios tipos posibles, pero solo uno a la vez. Todos los campos comparten la misma dirección de memoria.

La unión reserva espacio suficiente para su campo más grande, respetando también los requisitos de alineamiento.

const NumeroRaw = union {
    entero: i32,
    flotante: f64,
};

var n = NumeroRaw{ .entero = 10 };
// En memoria hay un 10.
// Al asignar otro valor a la unión, sobreescribimos esos bits.
n = NumeroRaw{ .flotante = 3.14 };
Copied!

Las uniones sin etiqueta (bare unions) son útiles para interoperabilidad con C y código de bajo nivel, pero no registran qué campo está activo. Acceder a un campo distinto del último escrito es comportamiento ilegal: los modos seguros pueden detectarlo, mientras que ReleaseFast y ReleaseSmall no tienen por qué hacerlo.

Uniones etiquetadas

Para asociar cada variante con el campo activo, Zig ofrece las uniones etiquetadas (tagged unions).

Combinamos un enum (la etiqueta o tag) con una union (los datos o payload). La etiqueta forma parte del valor y permite conocer qué campo está activo.

La sintaxis es union(EnumTag). Si no queremos crear el enum explícitamente, usamos union(enum).

// Definimos una Tagged Union
const Resultado = union(enum) {
    exito: u32,            // Si es éxito, lleva un número
    error_red: u16,        // Si es error red, lleva un código
    desconocido: void,     // Si es desconocido, no lleva datos
};
Copied!

Ahora tenemos un tipo polimórfico seguro. Una variable Resultado puede ser un número, un error o nada.

Manejo con switch

La forma habitual de acceder a los datos de una unión etiquetada es mediante switch. Así manejamos todos los casos y podemos capturar el payload de la rama activa.

const res = Resultado{ .exito = 100 };

switch (res) {
    // Capturamos el valor con |valor|
    .exito => |valor| std.debug.print("Ganaste {d} puntos\n", .{valor}),
    
    // Capturamos el código de error
    .error_red => |codigo| std.debug.print("Error HTTP {d}\n", .{codigo}),
    
    // Caso sin datos (void)
    .desconocido => std.debug.print("Ni idea de qué pasó\n", .{}),
}
Copied!

Intento de acceso inseguro: Si accedes a res.exito cuando la variante activa es .error_red, incurres en comportamiento ilegal. Los modos seguros lo detectan con un panic; un switch exhaustivo evita ese acceso incorrecto.

Uniones y polimorfismo

En lenguajes OOP, si quieres una lista de objetos diferentes (un Círculo y un Cuadrado), usas una clase base Figura y herencia.

En Zig, usamos una Tagged Union.

const Circulo = struct { radio: f32 };
const Cuadrado = struct { lado: f32 };

const Figura = union(enum) {
    circulo: Circulo,
    cuadrado: Cuadrado,
    
    pub fn area(self: Figura) f32 {
        return switch (self) {
            .circulo => |c| 3.14 * c.radio * c.radio,
            .cuadrado => |q| q.lado * q.lado,
        };
    }
};
Copied!

Este diseño representa un conjunto cerrado de variantes sin tablas virtuales ni asignaciones dinámicas obligatorias. El tamaño de Figura incluye la etiqueta, el espacio para la variante mayor y el relleno de alineamiento que corresponda.