Estructuras (structs) en Zig
Las estructuras son uno de los tipos de datos fundamentales en Zig que te permiten agrupar diferentes valores relacionados bajo un solo nombre. A diferencia de los arreglos (arrays), que solo pueden contener valores del mismo tipo, las estructuras pueden contener campos de diferentes tipos. Este concepto es similar a las estructuras en C o a las clases sin métodos en otros lenguajes.
En Zig, las estructuras son poderosas y flexibles, ofreciendo características como campos con valores por defecto, métodos asociados, funciones genéricas y mucho más.
Definición básica de una estructura
Para declarar una estructura en Zig, utilizamos la palabra clave struct
. Aquí tienes un ejemplo simple:
const Punto = struct {
x: i32,
y: i32,
};
En este ejemplo, hemos definido una estructura llamada Punto
con dos campos: x
e y
, ambos de tipo i32
.
Instanciación de estructuras
Una vez definida una estructura, podemos crear instancias de ella de varias maneras:
const std = @import("std");
const expect = std.testing.expect;
test "creación de estructuras" {
// Creación con todos los campos especificados
const p1 = Punto{ .x = 10, .y = 20 };
// Creación con algunos campos indefinidos (solo funciona con variables)
var p2 = Punto{ .x = 30, .y = undefined };
p2.y = 40; // Asignamos el valor más tarde
try expect(p1.x == 10);
try expect(p1.y == 20);
try expect(p2.x == 30);
try expect(p2.y == 40);
}
Estructuras dentro de estructuras
Las estructuras pueden incluir otras estructuras como campos:
const Rectangulo = struct {
esquina_superior_izquierda: Punto,
ancho: u32,
alto: u32,
};
test "estructuras anidadas" {
const rect = Rectangulo{
.esquina_superior_izquierda = Punto{ .x = 0, .y = 0 },
.ancho = 100,
.alto = 50,
};
try expect(rect.esquina_superior_izquierda.x == 0);
try expect(rect.ancho == 100);
}
Valores por defecto en campos
Zig permite asignar valores por defecto a los campos de una estructura:
const Configuracion = struct {
habilitado: bool = true,
intensidad: u8 = 50,
nombre: []const u8 = "predeterminado",
};
test "valores por defecto en campos" {
// Solo necesitamos especificar los campos que queremos personalizar
const conf = Configuracion{
.intensidad = 75,
};
try expect(conf.habilitado == true); // Valor por defecto
try expect(conf.intensidad == 75); // Valor personalizado
try expect(std.mem.eql(u8, conf.nombre, "predeterminado")); // Valor por defecto
}
Métodos en estructuras
En Zig, las estructuras pueden tener métodos asociados. Es importante entender que estos no son métodos especiales como en lenguajes orientados a objetos, sino simplemente funciones que están en el espacio de nombres de la estructura:
const Vector = struct {
x: f32,
y: f32,
// Método para crear un vector
pub fn crear(x: f32, y: f32) Vector {
return Vector{ .x = x, .y = y };
}
// Método para calcular la magnitud del vector
pub fn magnitud(self: Vector) f32 {
return @sqrt(self.x * self.x + self.y * self.y);
}
// Método para sumar dos vectores
pub fn sumar(self: Vector, otro: Vector) Vector {
return Vector{
.x = self.x + otro.x,
.y = self.y + otro.y,
};
}
};
test "métodos de estructura" {
const v1 = Vector.crear(3.0, 4.0);
try expect(v1.magnitud() == 5.0);
const v2 = Vector{ .x = 1.0, .y = 2.0 };
const v3 = v1.sumar(v2);
try expect(v3.x == 4.0);
try expect(v3.y == 6.0);
// También podemos llamar al método como una función normal
const mag = Vector.magnitud(v1);
try expect(mag == 5.0);
}
En este ejemplo, observe cómo el primer parámetro de cada método representa la instancia de la estructura. Por convención, se nombra self
, pero podría tener cualquier nombre. Los métodos pueden ser llamados con la sintaxis de punto (v1.magnitud()
) o como funciones normales (Vector.magnitud(v1)
).
Estructuras con alineación personalizada
La alineación de memoria es importante para el rendimiento de la CPU. Zig permite controlar la alineación de las estructuras y sus campos:
const DatosAlineados = struct {
// Campo alineado a 8 bytes
valor: u32 align(8),
// Campo normal
bandera: bool,
};
// Estructura entera alineada a 16 bytes
const EstructuraAlineada = extern struct {
x: f32,
y: f32,
};
test "alineación de estructuras" {
var datos = DatosAlineados{
.valor = 123,
.bandera = true,
};
// Comprobar que el campo está correctamente alineado
try expect(@TypeOf(&datos.valor) == *align(8) u32);
}
Estructuras empaquetadas (packed)
Normalmente, Zig (como la mayoría de los lenguajes) añade relleno entre los campos de una estructura para optimizar el acceso. Sin embargo, a veces queremos un control exacto sobre la disposición de los bytes, por ejemplo, cuando trabajamos con formatos de archivo o hardware. Para esto, Zig ofrece estructuras empaquetadas:
const Color = packed struct {
rojo: u8,
verde: u8,
azul: u8,
alfa: u8,
};
test "estructura empaquetada" {
var color = Color{
.rojo = 255,
.verde = 128,
.azul = 64,
.alfa = 255,
};
// En una estructura empaquetada, se garantiza que los campos
// están ordenados exactamente como se declararon
const puntero_bytes = @as([*]const u8, @ptrCast(&color));
try expect(puntero_bytes[0] == 255); // rojo
try expect(puntero_bytes[1] == 128); // verde
try expect(puntero_bytes[2] == 64); // azul
try expect(puntero_bytes[3] == 255); // alfa
}
Estructuras externas
Las estructuras externas son útiles cuando necesitamos interactuar con código en C o con otro código externo:
const TiempoC = extern struct {
segundos: c_long,
nanosegundos: c_long,
};
// Esta estructura tendrá la misma disposición en memoria que su equivalente en C
Las estructuras externas siguen las reglas de alineación y empaquetado del ABI de C para la plataforma objetivo.
Estructuras anónimas
Zig permite crear estructuras sin nombre, que son útiles para valores temporales o para implementar genéricos:
test "estructuras anónimas" {
const punto = struct {
x: i32,
y: i32,
}{ .x = 10, .y = 20 };
try expect(punto.x == 10);
try expect(punto.y == 20);
}
Estructuras como espacio de nombres
Las estructuras también pueden servir como espacios de nombres, agrupando constantes, variables y funciones relacionadas:
const Matematicas = struct {
pub const PI = 3.14159;
pub const E = 2.71828;
pub fn seno(x: f32) f32 {
// Implementación simple para el ejemplo
return @sin(x);
}
pub fn coseno(x: f32) f32 {
return @cos(x);
}
};
test "estructura como espacio de nombres" {
try expectApprox(Matematicas.PI, 3.14159);
try expectApprox(Matematicas.seno(0.0), 0.0);
try expectApprox(Matematicas.coseno(0.0), 1.0);
}
fn expectApprox(a: f32, b: f32) !void {
const epsilon = 0.0001;
try expect(@abs(a - b) < epsilon);
}
Variables estáticas en estructuras
Las estructuras pueden contener variables estáticas, que son útiles para implementar patrones como singletons o contadores compartidos:
const Contador = struct {
// Variable estática compartida por todas las instancias
var valor: u32 = 0;
id: u32,
pub fn crear() Contador {
valor += 1;
return Contador{
.id = valor,
};
}
pub fn obtenerValorActual() u32 {
return valor;
}
};
test "variables estáticas en estructuras" {
const c1 = Contador.crear();
const c2 = Contador.crear();
const c3 = Contador.crear();
try expect(c1.id == 1);
try expect(c2.id == 2);
try expect(c3.id == 3);
try expect(Contador.obtenerValorActual() == 3);
}
Estructuras genéricas con comptime
Una de las características más poderosas de Zig es la capacidad de crear tipos genéricos utilizando funciones que se ejecutan en tiempo de compilación:
fn Lista(comptime T: type) type {
return struct {
const Self = @This();
elementos: []T,
longitud: usize,
pub fn inicializar(elementos: []T) Self {
return Self{
.elementos = elementos,
.longitud = elementos.len,
};
}
pub fn obtener(self: Self, indice: usize) T {
if (indice >= self.longitud) {
@panic("Índice fuera de rango");
}
return self.elementos[indice];
}
};
}
test "estructuras genéricas" {
var numeros = [_]i32{ 10, 20, 30, 40 };
// Crear una lista de enteros
const ListaDeEnteros = Lista(i32);
var lista = ListaDeEnteros.inicializar(numeros[0..]);
try expect(lista.longitud == 4);
try expect(lista.obtener(2) == 30);
// También podemos dejar que Zig infiera el tipo
var letras = [_]u8{ 'a', 'b', 'c' };
var lista_letras = Lista(u8).inicializar(letras[0..]);
try expect(lista_letras.obtener(0) == 'a');
}
Esta técnica es extremadamente poderosa y flexible, permitiendo crear estructuras de datos que funcionan con cualquier tipo.
Desestructuración de estructuras
Zig no proporciona una sintaxis de desestructuración para estructuras como en otros lenguajes. Para acceder a múltiples campos, se debe hacer de manera individual:
const Persona = struct {
nombre: []const u8,
edad: u8,
altura: f32,
};
test "acceso a campos de estructura" {
const p = Persona{
.nombre = "Ana",
.edad = 28,
.altura = 1.75,
};
const nombre = p.nombre;
const edad = p.edad;
try expect(std.mem.eql(u8, nombre, "Ana"));
try expect(edad == 28);
}
Conclusión
Las estructuras en Zig son extremadamente versátiles y potentes. Permiten organizar datos relacionados, crear tipos personalizados, implementar métodos asociados y construir abstracciones genéricas. A pesar de su simplicidad conceptual, las estructuras son uno de los bloques de construcción más importantes en programas Zig.
Algunas ventajas clave de las estructuras en Zig incluyen:
- Control preciso sobre la disposición en memoria (con
packed
yextern
). - Capacidad para crear tipos genéricos mediante funciones de tiempo de compilación.
- Claridad en la distinción entre datos y comportamiento.
- Flexibilidad para usar estructuras como espacios de nombres o tipos de datos.
Al dominar el uso de estructuras, tendrás una herramienta fundamental para escribir código Zig claro, eficiente y bien organizado.