Ir al contenido principal

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:

  1. Control preciso sobre la disposición en memoria (con packed y extern).
  2. Capacidad para crear tipos genéricos mediante funciones de tiempo de compilación.
  3. Claridad en la distinción entre datos y comportamiento.
  4. 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.