Ir al contenido principal

Capturas de carga útil en Zig (payload capture)

Las capturas de carga útil (payload capture) son un mecanismo fundamental en Zig que permite extraer y utilizar valores encapsulados dentro de estructuras compuestas como tipos opcionales, uniones etiquetadas y uniones de error. Esta técnica es especialmente potente cuando se combina con expresiones de control como if, while y switch, facilitando un código más seguro y expresivo.

En este artículo, exploraremos en profundidad cómo funcionan las capturas de carga útil, cuándo utilizarlas y las distintas formas en que podemos aprovechar esta característica para escribir código Zig más elegante y robusto.

Fundamentos de las capturas de carga útil

Una captura de carga útil se realiza usando la sintaxis de barra vertical (|valor|), donde "valor" es el identificador que queremos asignar al contenido extraído. Esta sintaxis aparece en expresiones de control específicas que pueden trabajar con tipos que encapsulan otros valores.

Tipos que admiten capturas de carga útil

En Zig, tres tipos principales admiten capturas de carga útil:

  1. Tipos opcionales (?T): Pueden contener un valor de tipo T o ser null.
  2. Uniones de error (anyerror!T): Pueden contener un valor de tipo T o un error.
  3. Uniones etiquetadas: Uniones con una etiqueta enum que indica qué campo está activo.

Veamos ejemplos básicos de cada uno:

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

test "capturas básicas de carga útil" {
    // Con tipo opcional
    const opcional: ?i32 = 42;
    if (opcional) |valor| {
        try expect(valor == 42);
    }

    // Con unión de error
    const resultado: anyerror!i32 = 42;
    if (resultado) |valor| {
        try expect(valor == 42);
    } else |err| {
        _ = err;
        unreachable;
    }

    // Con unión etiquetada
    const Valor = union(enum) {
        entero: i32,
        flotante: f32,
    };
    
    const val = Valor{ .entero = 42 };
    switch (val) {
        .entero => |n| try expect(n == 42),
        .flotante => |f| _ = f,
    }
}

Capturas de carga útil con tipos opcionales

Los tipos opcionales (?T) son una forma segura de representar valores que pueden estar ausentes. Usando capturas de carga útil, podemos extraer y trabajar con el valor interno cuando está presente.

Uso básico con if

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

test "captura de carga útil con opcional en if" {
    const numero: ?i32 = 10;
    
    // La variable 'valor' solo existe dentro de este bloque
    // y contiene el valor desenvuelto (10)
    if (numero) |valor| {
        try expect(valor == 10);
        // Aquí 'valor' es de tipo i32, no ?i32
    } else {
        unreachable; // No llegaremos aquí porque numero no es null
    }

    // Otro ejemplo con null
    const vacio: ?i32 = null;
    if (vacio) |valor| {
        _ = valor;
        unreachable; // No llegaremos aquí porque vacio es null
    } else {
        // Este bloque se ejecuta porque vacio es null
        try expect(true);
    }
}

Modificando valores a través de capturas por referencia

Una característica potente es la capacidad de modificar el valor original agregando un asterisco (*) a la captura:

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

test "modificando valores opcionales a través de capturas" {
    var numero: ?i32 = 10;
    
    if (numero) |*valor| {
        // valor es un puntero al contenido de numero
        valor.* += 5;
    }
    
    try expect(numero.? == 15); // El valor original ha sido modificado
}

Uso con while

Las capturas de carga útil también funcionan en bucles while, lo que es especialmente útil para procesar secuencias de valores opcionales:

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

test "capturas de carga útil con while y opcionales" {
    var indice: usize = 0;
    const numeros = [_]?i32{ 1, 2, null, 4, 5 };
    
    var suma: i32 = 0;
    while (indice < numeros.len and numeros[indice] != null) : (indice += 1) {
        if (numeros[indice]) |valor| {
            suma += valor;
        }
    }
    
    try expect(suma == 3); // 1 + 2, se detiene al encontrar null
}

Capturas de carga útil con uniones de error

Las uniones de error combinan un tipo con un posible error, lo que permite una gestión elegante de errores. Las capturas de carga útil nos ayudan a extraer tanto el valor exitoso como el error.

Captura básica con if

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

fn siempreExito() !i32 {
    return 42;
}

fn siempreFallo() !i32 {
    return error.Fallo;
}

test "capturas de carga útil con uniones de error" {
    // Captura de valor exitoso
    if (siempreExito()) |valor| {
        try expect(valor == 42);
    } else |err| {
        _ = err;
        unreachable; // No llegaremos aquí
    }
    
    // Captura de error
    if (siempreFallo()) |valor| {
        _ = valor;
        unreachable; // No llegaremos aquí
    } else |err| {
        try expect(err == error.Fallo);
    }
}

Modificando valores a través de capturas por referencia

Similar a los opcionales, podemos modificar el valor exitoso capturado:

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

test "modificando valores en uniones de error" {
    var resultado: anyerror!i32 = 10;
    
    if (resultado) |*valor| {
        valor.* += 5;
    } else |_| {
        unreachable;
    }
    
    // Verificamos que el valor original ha sido modificado
    const valorFinal = resultado catch unreachable;
    try expect(valorFinal == 15);
}

Combinación con try y catch

Las capturas de carga útil se integran perfectamente con operadores como try y catch:

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

fn procesar(valor: i32) !i32 {
    if (valor < 0) return error.ValorNegativo;
    return valor * 2;
}

test "capturas de carga útil con try y catch" {
    // Usando try (propaga el error si ocurre)
    const resultado1 = try procesar(5);
    try expect(resultado1 == 10);
    
    // Usando catch con captura de error
    const resultado2 = procesar(-5) catch |err| {
        try expect(err == error.ValorNegativo);
        return;
    };
    
    // No llegaremos aquí si procesar falla
    _ = resultado2;
}

Capturas de carga útil con uniones etiquetadas

Las uniones etiquetadas en Zig permiten almacenar diferentes tipos de datos en una misma estructura, junto con una etiqueta que indica qué tipo está activo. Las capturas de carga útil son especialmente útiles con switch para manejar todos los posibles casos.

Uso básico con switch

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

test "capturas de carga útil con uniones etiquetadas" {
    const Valor = union(enum) {
        entero: i32,
        flotante: f32,
        texto: []const u8,
    };
    
    const valores = [_]Valor{
        Valor{ .entero = 42 },
        Valor{ .flotante = 3.14 },
        Valor{ .texto = "hola" },
    };
    
    for (valores) |val| {
        switch (val) {
            .entero => |n| {
                try expect(n == 42);
            },
            .flotante => |f| {
                try expect(f == 3.14);
            },
            .texto => |t| {
                try expect(std.mem.eql(u8, t, "hola"));
            },
        }
    }
}

Modificando valores en switch

También podemos modificar los valores capturados en un switch:

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

test "modificando valores en capturas de switch" {
    const Punto = struct {
        x: u8,
        y: u8,
    };
    
    const Figura = union(enum) {
        circulo: f32,      // radio
        rectangulo: Punto, // ancho y alto
    };
    
    var figura = Figura{ .rectangulo = Punto{ .x = 10, .y = 20 } };
    
    switch (figura) {
        .circulo => |*r| {
            r.* *= 2.0;
        },
        .rectangulo => |*p| {
            p.*.x *= 2;
            p.*.y *= 2;
        },
    }
    
    // Verificamos que los cambios se aplicaron
    if (figura == .rectangulo) {
        const rect = figura.rectangulo;
        try expect(rect.x == 20);
        try expect(rect.y == 40);
    }
}

Captura de datos y etiquetas

En un caso más avanzado, podemos capturar tanto el valor como la etiqueta en una unión etiquetada:

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

test "capturas de valor y etiqueta en uniones" {
    const Dato = union(enum) {
        numero: i32,
        booleano: bool,
    };
    
    const dato = Dato{ .numero = 123 };
    
    switch (dato) {
        // Aquí capturamos tanto el valor como la etiqueta
        inline else => |valor, etiqueta| {
            if (etiqueta == .numero) {
                try expect(valor == 123);
            } else {
                unreachable;
            }
        },
    }
}

Casos de uso avanzados

Anidamiento de capturas

Las capturas de carga útil pueden anidarse, lo que es útil para tipos complejos:

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

test "capturas de carga útil anidadas" {
    // Un opcional que contiene una unión de error
    const v: ?anyerror!i32 = 42;
    
    if (v) |resultado| {
        // Primero desenvuelve el opcional, luego la unión de error
        if (resultado) |valor| {
            try expect(valor == 42);
        } else |_| {
            unreachable;
        }
    } else {
        unreachable;
    }
}

Capturas en operaciones complejas

Las capturas de carga útil brillan en operaciones como el procesamiento de estructuras de datos:

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

// Un nodo de una lista enlazada
const Nodo = struct {
    valor: i32,
    siguiente: ?*Nodo,
    
    fn crear(valor: i32) !*Nodo {
        const nodo = try std.heap.page_allocator.create(Nodo);
        nodo.* = Nodo{
            .valor = valor,
            .siguiente = null,
        };
        return nodo;
    }
};

test "capturas en una lista enlazada" {
    var nodo1 = try Nodo.crear(1);
    defer std.heap.page_allocator.destroy(nodo1);
    
    const nodo2 = try Nodo.crear(2);
    defer std.heap.page_allocator.destroy(nodo2);
    
    nodo1.siguiente = nodo2;
    
    // Recorremos la lista usando capturas de carga útil
    var suma: i32 = 0;
    var actual: ?*Nodo = nodo1;
    
    while (actual) |nodo| {
        suma += nodo.valor;
        actual = nodo.siguiente;
    }
    
    try expect(suma == 3); // 1 + 2
}

Consideraciones y buenas prácticas

Cuándo usar capturas de carga útil

Las capturas de carga útil son especialmente útiles cuando:

  1. Necesitas extraer y utilizar el valor interno de un tipo contenedor (opcional, unión de error, unión etiquetada).
  2. Quieres verificar el estado de un valor y actuar según su contenido.
  3. Necesitas modificar el valor original a través de una referencia.

Alternativas a las capturas de carga útil

Aunque las capturas son elegantes, a veces otros enfoques son más claros:

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

test "alternativas a capturas de carga útil" {
    // Con capturas
    const opcional: ?i32 = 42;
    if (opcional) |valor| {
        try expect(valor == 42);
    }
    
    // Sin capturas (alternativa)
    if (opcional != null) {
        try expect(opcional.? == 42); // Usamos desenvuelto directo
    }
    
    // Con capturas para unión de error
    const resultado: anyerror!i32 = 42;
    if (resultado) |valor| {
        try expect(valor == 42);
    } else |_| {}
    
    // Sin capturas (alternativa)
    const valorResultado = resultado catch unreachable;
    try expect(valorResultado == 42);
}

Depuración con capturas de carga útil

Las capturas de carga útil facilitan la depuración, ya que permiten inspeccionar valores con claridad:

const std = @import("std");

fn depurarDato(dato: ?i32) void {
    if (dato) |valor| {
        std.debug.print("Dato presente: {}\n", .{valor});
    } else {
        std.debug.print("Dato ausente (null)\n", .{});
    }
}

test "depuración de carga útil" {
    const opcional: ?i32 = 42;
    depurarDato(opcional); // Imprime: Dato presente: 42

    const vacio: ?i32 = null;
    depurarDato(vacio); // Imprime: Dato ausente (null)
}

Conclusión

Las capturas de carga útil (payload capture) en Zig representan un mecanismo potente y elegante para trabajar con tipos que encapsulan valores. A través de esta característica, podemos extraer, inspeccionar y modificar datos de manera segura, evitando errores comunes como desreferenciar punteros nulos o ignorar errores.

Esta sintaxis se integra perfectamente con las estructuras de control del lenguaje, permitiendo código más limpio y expresivo. Al dominar las capturas de carga útil, los programadores de Zig pueden escribir código más robusto y mantenible, aprovechando al máximo el sistema de tipos de Zig.

Las capturas de carga útil son un ejemplo claro de cómo Zig prioriza la seguridad y la claridad sin sacrificar el control. Con las herramientas y ejemplos proporcionados en este artículo, ahora puedes aprovechar plenamente esta característica en tus propios proyectos.