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.