Ir al contenido principal

Enumeraciones (enum) en Zig

 

Las enumeraciones, comúnmente conocidas como enum, son una característica poderosa en muchos lenguajes de programación, y Zig no es una excepción. Los enums en Zig permiten definir un tipo que puede tomar uno de varios valores predefinidos, lo que los hace ideales para modelar conjuntos de opciones mutuamente excluyentes. En este artículo, exploraremos en profundidad el concepto de enumeraciones en Zig, sus características, y veremos ejemplos prácticos de cómo utilizarlas efectivamente en nuestros programas.

¿Qué es un enum en Zig?

En Zig, un enum es un tipo definido por el usuario que consiste en un conjunto de valores con nombre. Cada valor del enum se llama "tag" o etiqueta, y representa un valor entero constante. Los enums son útiles cuando necesitamos definir un conjunto finito de posibles valores que una variable puede tomar.

Veamos un ejemplo básico:

const std = @import("std");
const expect = std.testing.expect;

const Color = enum {
    rojo,
    verde,
    azul,
};

test "enum básico" {
    const mi_color = Color.verde;
    try expect(mi_color == Color.verde);
}

En este ejemplo, hemos definido un enum llamado Color con tres posibles valores: rojo, verde y azul. Luego, creamos una constante mi_color con el valor Color.verde y verificamos que sea igual a Color.verde.

Valores ordinales en enums

Por defecto, Zig asigna automáticamente valores ordinales a los elementos del enum, comenzando desde 0 y aumentando en 1 para cada elemento. Podemos acceder a estos valores ordinales utilizando la función @intFromEnum:

const std = @import("std");
const expect = std.testing.expect;

const Direccion = enum {
    norte,  // 0
    sur,    // 1
    este,   // 2
    oeste,  // 3
};

test "valores ordinales de enum" {
    try expect(@intFromEnum(Direccion.norte) == 0);
    try expect(@intFromEnum(Direccion.sur) == 1);
    try expect(@intFromEnum(Direccion.este) == 2);
    try expect(@intFromEnum(Direccion.oeste) == 3);
}

Especificación del tipo de dato para el enum

También podemos especificar explícitamente el tipo de dato subyacente que se utilizará para representar los valores del enum:

const std = @import("std");
const expect = std.testing.expect;

const EstadoProceso = enum(u8) {
    inactivo,
    ejecutando,
    pausado,
    terminado,
};

test "tipo de dato de enum" {
    try expect(@TypeOf(@intFromEnum(EstadoProceso.inactivo)) == u8);
}

En este caso, hemos especificado que queremos que nuestro enum EstadoProceso utilice un u8 como tipo de dato subyacente. Esto es útil cuando necesitamos controlar el tamaño de memoria que ocupará el enum o cuando queremos asegurarnos de que sea compatible con alguna API específica.

Valores personalizados para elementos del enum

Podemos asignar valores específicos a los elementos del enum:

const std = @import("std");
const expect = std.testing.expect;

const CodigoError = enum(u16) {
    ninguno = 0,
    no_encontrado = 404,
    no_autorizado = 401,
    servidor = 500,
};

test "valores personalizados de enum" {
    try expect(@intFromEnum(CodigoError.ninguno) == 0);
    try expect(@intFromEnum(CodigoError.no_encontrado) == 404);
    try expect(@intFromEnum(CodigoError.no_autorizado) == 401);
    try expect(@intFromEnum(CodigoError.servidor) == 500);
}

En este ejemplo, hemos asignado valores específicos a los elementos de nuestro enum CodigoError. Los elementos que no tienen un valor asignado explícitamente recibirán automáticamente un valor que es uno más que el valor del elemento anterior.

const std = @import("std");
const expect = std.testing.expect;

const MixtoEnum = enum(u8) {
    a = 10,
    b,      // 11 (automáticamente 10 + 1)
    c = 20,
    d,      // 21 (automáticamente 20 + 1)
    e,      // 22 (automáticamente 21 + 1)
};

test "valores mixtos en enum" {
    try expect(@intFromEnum(MixtoEnum.a) == 10);
    try expect(@intFromEnum(MixtoEnum.b) == 11);
    try expect(@intFromEnum(MixtoEnum.c) == 20);
    try expect(@intFromEnum(MixtoEnum.d) == 21);
    try expect(@intFromEnum(MixtoEnum.e) == 22);
}

Conversión entre enums y enteros

Como hemos visto, podemos convertir un enum a su valor entero correspondiente usando @intFromEnum. De manera similar, podemos convertir un entero a un enum utilizando @enumFromInt:

const std = @import("std");
const expect = std.testing.expect;

const Dia = enum {
    lunes,
    martes,
    miercoles,
    jueves,
    viernes,
    sabado,
    domingo,
};

test "conversión entre enum y entero" {
    const dia_numero = 3; // jueves (índice 3)
    const dia = @as(Dia, @enumFromInt(dia_numero));
    try expect(dia == Dia.jueves);
    try expect(@intFromEnum(dia) == dia_numero);
}

Es importante destacar que si intentamos convertir un valor entero que no corresponde a un elemento válido del enum, se producirá un error en tiempo de ejecución (en modo seguro) o un comportamiento indefinido (en modo de rendimiento).

Métodos en enums

Al igual que las estructuras, los enums en Zig pueden tener métodos. Esto es especialmente útil para añadir comportamiento específico relacionado con el enum:

const std = @import("std");
const expect = std.testing.expect;

const Palo = enum {
    treboles,
    diamantes,
    corazones,
    picas,

    pub fn esRojo(self: Palo) bool {
        return self == Palo.corazones or self == Palo.diamantes;
    }

    pub fn esNegro(self: Palo) bool {
        return self == Palo.treboles or self == Palo.picas;
    }
};

test "métodos en enum" {
    const palo = Palo.corazones;
    try expect(palo.esRojo());
    try expect(!palo.esNegro());
    
    // También podemos llamar al método directamente desde el tipo
    try expect(Palo.esRojo(Palo.diamantes));
    try expect(Palo.esNegro(Palo.treboles));
}

En este ejemplo, hemos añadido dos métodos a nuestro enum Palo: esRojo y esNegro. Estos métodos nos permiten consultar si un palo específico es rojo o negro, respectivamente.

Switch con enums

Los enums son especialmente útiles cuando se combinan con la expresión switch. Zig garantiza la comprobación exhaustiva, lo que significa que el compilador verificará que todos los posibles valores del enum sean manejados en el switch:

const std = @import("std");
const expect = std.testing.expect;

const EstadoJuego = enum {
    inicio,
    jugando,
    pausa,
    fin,
};

fn obtenerMensaje(estado: EstadoJuego) []const u8 {
    return switch (estado) {
        .inicio => "¡Bienvenido al juego!",
        .jugando => "Juego en progreso...",
        .pausa => "Juego en pausa",
        .fin => "Juego terminado. ¡Gracias por jugar!",
        // No es necesario un caso 'else' porque Zig sabe que hemos cubierto todos los casos
    };
}

test "switch con enum" {
    try expect(std.mem.eql(u8, obtenerMensaje(EstadoJuego.inicio), "¡Bienvenido al juego!"));
    try expect(std.mem.eql(u8, obtenerMensaje(EstadoJuego.pausa), "Juego en pausa"));
}

Si añadimos un nuevo valor al enum pero olvidamos manejarlo en el switch, el compilador nos dará un error, lo que ayuda a prevenir errores comunes.

Enums no exhaustivos

En algunos casos, podemos querer definir un enum que podría tener valores adicionales no especificados explícitamente. Esto es útil cuando interactuamos con sistemas externos o cuando queremos dejar espacio para futuras extensiones. Para ello, podemos utilizar un enum no exhaustivo añadiendo un guion bajo _ como último elemento:

const std = @import("std");
const expect = std.testing.expect;

const EstadoHTTP = enum(u16) {
    ok = 200,
    created = 201,
    // ... otros códigos de éxito
    bad_request = 400,
    unauthorized = 401,
    not_found = 404,
    // ... otros códigos de error
    _, // Indica que puede haber otros valores no listados
};

test "enum no exhaustivo" {
    const estado = @as(EstadoHTTP, @enumFromInt(418)); // Código 418: I'm a teapot
    try expect(@intFromEnum(estado) == 418);
}

Con un enum no exhaustivo, podemos usar @enumFromInt para convertir cualquier valor entero dentro del rango del tipo subyacente a un valor del enum, incluso si ese valor no está listado explícitamente.

Uso de literales de enum

Zig permite el uso de "literales de enum" que son una forma abreviada de referirse a valores de enum cuando el tipo se puede inferir del contexto:

const std = @import("std");
const expect = std.testing.expect;

const Tamano = enum {
    pequeno,
    mediano,
    grande,
};

fn comprobarTamano(tamano: Tamano) bool {
    return switch (tamano) {
        .pequeno => false,
        .mediano => true,
        .grande => true,
    };
}

test "literales de enum" {
    const tamano: Tamano = .mediano;  // Equivalente a Tamaño.mediano
    try expect(comprobarTamano(tamano));
    try expect(comprobarTamano(.grande));  // Podemos usar directamente el literal
}

Enums para sistemas embebidos y FFI

Los enums son especialmente útiles cuando trabajamos con sistemas embebidos o cuando necesitamos interoperar con código C a través de la Interfaz de Función Foránea (FFI). Al poder especificar el tipo subyacente y asignar valores específicos, podemos asegurarnos de que nuestros enums sean compatibles con las especificaciones de las APIs externas:

const std = @import("std");
const expect = std.testing.expect;

const PinGPIO = enum(u8) {
    pin0 = 0,
    pin1 = 1,
    pin2 = 2,
    // ... otros pines
    pin13 = 13, // LED en Arduino Uno
    _, // No exhaustivo para permitir cualquier pin válido
};

// Función que simula la configuración de un pin GPIO
fn configurarPin(pin: PinGPIO, como_salida: bool) bool {
    // Lógica real aquí...
    _ = como_salida; // Simulamos que se configura el pin
    return @intFromEnum(pin) <= 20; // Supongamos que solo los pines 0-20 son válidos
}

test "enum para hardware" {
    try expect(configurarPin(.pin13, true));
    const pin_arbitrario = @as(PinGPIO, @enumFromInt(5));
    try expect(configurarPin(pin_arbitrario, false));
}

Obtener nombres de campos de enum

A veces es útil obtener el nombre de un valor de enum como una cadena, especialmente para depuración o para interfaces de usuario. Zig proporciona la función @tagName para esto:

const std = @import("std");
const expect = std.testing.expect;

const Animal = enum {
    perro,
    gato,
    pajaro,
    pez,
};

test "obtener nombre de enum" {
    const animal = Animal.gato;
    try expect(std.mem.eql(u8, @tagName(animal), "gato"));
}

Conclusión

Los enums en Zig son una herramienta poderosa y flexible para modelar conjuntos de valores discretos. Con características como tipos subyacentes personalizables, valores asignados explícitamente, métodos y garantías de comprobación exhaustiva, los enums en Zig ofrecen una combinación perfecta de seguridad tipo y flexibilidad.

Cuando los utilices en tus programas, aprovecha estas características para crear código más claro, seguro y expresivo. Los enums son especialmente útiles para representar estados, categorías, opciones o cualquier otro conjunto de valores mutuamente excluyentes.