zig-structs-datos-complejos

Structs en Zig: composición, métodos y datos complejos

  • 5 min

Un struct en Zig es un tipo compuesto que agrupa campos relacionados bajo un mismo nombre. Hasta ahora hemos trabajado con tipos simples o colecciones de un mismo tipo, pero el mundo real no es tan uniforme.

Un “Usuario” tiene un nombre (texto), una edad (entero) y un estado (booleano). Necesitamos una forma de empaquetar estos datos heterogéneos en una sola unidad lógica.

En Zig, esta herramienta es el struct.

Si vienes de C++, Java o C#, quizá pienses en los structs como clases. Aunque ambos agrupan datos y comportamiento, Zig no incorpora herencia ni despacho dinámico implícito.

Declaración e inicialización

Un struct en Zig se declara asignando la definición a una constante. Esto es importante: el tipo es un valor que se conoce en tiempo de compilación.

const Jugador = struct {
    nombre: []const u8,
    puntos: u32,
    activo: bool,
};
Copied!

Para crear una instancia de este Jugador, usamos una sintaxis muy explícita donde nombramos cada campo.

const p1 = Jugador{
    .nombre = "Luis",
    .puntos = 1500,
    .activo = true,
};
Copied!

Fíjate en el punto . antes de cada campo (.nombre). Zig lo usa para diferenciar claramente la asignación de campos de otras operaciones. Además, el orden de inicialización no importa, pero debes inicializar todos los campos.

Métodos: funciones dentro de structs

Zig no tiene “métodos” en el sentido estricto de que pertenezcan a un objeto. Simplemente permite escribir funciones dentro del espacio de nombres (namespace) del struct.

Para definir operaciones sobre el valor usamos por convención el parámetro self.

Funciones de inicialización

Si una función no toma el struct como parámetro, es una función estática. Por convención, solemos crear una llamada init para actuar como constructor.

const Jugador = struct {
    puntos: u32,

    // Función "estática" (no recibe self)
    pub fn init(puntos_iniciales: u32) Jugador {
        return Jugador{
            .puntos = puntos_iniciales,
        };
    }
};

// Uso:
const p = Jugador.init(100);
Copied!

Métodos de instancia

Si el primer parámetro de la función es el propio struct (o un puntero a él), podemos llamarla usando la sintaxis de punto objeto.funcion().

const Jugador = struct {
    puntos: u32,

    // Recibe un puntero mutable a sí mismo
    pub fn ganarPuntos(self: *Jugador, cantidad: u32) void {
        self.puntos += cantidad;
    }
    
    // Recibe un puntero de solo lectura
    pub fn imprimir(self: *const Jugador) void {
        std.debug.print("Puntos: {d}\n", .{self.puntos});
    }
};

pub fn main() void {
    var p = Jugador{ .puntos = 100 };
    
    // Syntactic Sugar: Zig convierte esto en Jugador.ganarPuntos(&p, 50)
    p.ganarPuntos(50); 
    p.imprimir();
}
Copied!

Nota: self no es una palabra reservada especial (como this en Java). Es solo el nombre que usamos por convención para el primer argumento. Podrías llamarlo este o contexto, pero usa self para que todos te entiendan.

Structs y clases en memoria

En Java los objetos incluyen metadatos administrados por el runtime. En C++, una clase con funciones virtuales suele incorporar un puntero a su tabla virtual.

Un struct normal de Zig no añade una cabecera de objeto ni una tabla virtual por sí mismo. Sí puede contener relleno de alineamiento entre campos.

Esto garantiza que:

  1. Sin despacho implícito: cualquier mecanismo de polimorfismo debe expresarse en el código.
  2. Representación directa: el valor contiene sus campos y el relleno necesario para alinearlos.
  3. Datos contiguos: los arrays de structs almacenan sus elementos uno junto a otro.

Reordenamiento de campos

Una curiosidad técnica: Zig se reserva el derecho de reordenar los campos de tu struct en memoria para optimizar el alineamiento y gastar menos RAM (padding).

Si necesitas una representación compatible con la ABI de C, usa extern struct. Para campos definidos a nivel de bit existe packed struct; no son intercambiables y el orden de bytes sigue dependiendo de la arquitectura o del formato externo.

Valores por defecto

Podemos asignar valores por defecto a los campos del struct. Esto nos permite omitirlos al inicializar.

const Configuracion = struct {
    puerto: u16 = 8080,
    host: []const u8 = "localhost",
    debug: bool = false,
};

// Podemos inicializar vacío y coge los defaults
const config = Configuracion{}; 

// O sobreescribir solo lo que queramos
const config_prod = Configuracion{ .debug = true };
Copied!

No hay herencia, hay composición

Zig no tiene herencia de clases. No puedes hacer struct Perro extends Animal.

La filosofía de Zig (y de muchos lenguajes modernos como Go o Rust) es: Prefiere la composición sobre la herencia.

Si quieres reutilizar código, simplemente pon un struct dentro de otro.

const Motor = struct {
    caballos: u32,
};

const Coche = struct {
    marca: []const u8,
    // Composición: El coche TIENE UN motor
    motor: Motor, 
};
Copied!

Esto hace que las dependencias sean explícitas. No hay jerarquías complejas donde un cambio en la clase “Abuelo” rompe el código del “Nieto”.

Structs anónimos

A veces necesitamos agrupar datos rápidamente para un solo uso (por ejemplo, en el .{} del debug.print). Zig permite structs literales sin nombre.

// .{} es un struct anónimo vacío
std.debug.print("Hola", .{}); 

// Struct anónimo con campos
const punto = .{ .x = 10, .y = 20 };
Copied!