Tagged Union em Zig — O que é e Como Usar

Tagged Union em Zig — O que é e Como Usar

Definição

Uma tagged union (union discriminada ou union etiquetada) em Zig é um tipo que pode armazenar um dentre vários tipos de valor, junto com uma “tag” (etiqueta) que indica qual variante está ativa no momento. A tag é tipicamente um enum e é mantida automaticamente pelo compilador.

Diferentemente de unions em C (que são “burras” e não sabem qual campo está ativo), tagged unions em Zig são seguras: o compilador garante que você só acesse o campo correto.

Por que Tagged Unions Importam

  1. Modelagem de dados variantes: Representam valores que podem ser de tipos diferentes (ex: token de parser, mensagem de rede).
  2. Segurança: O compilador impede acesso ao campo errado.
  3. Switch exaustivo: Obriga tratamento de todas as variantes.
  4. Eficiência: Ocupam apenas o tamanho do maior campo + a tag.

Exemplo Prático

Definição e Uso

const std = @import("std");

const Forma = union(enum) {
    circulo: f64,          // raio
    retangulo: struct {
        largura: f64,
        altura: f64,
    },
    triangulo: struct {
        base: f64,
        altura: f64,
    },

    pub fn area(self: Forma) f64 {
        return switch (self) {
            .circulo => |raio| std.math.pi * raio * raio,
            .retangulo => |r| r.largura * r.altura,
            .triangulo => |t| (t.base * t.altura) / 2.0,
        };
    }
};

pub fn main() void {
    const c = Forma{ .circulo = 5.0 };
    const r = Forma{ .retangulo = .{ .largura = 10, .altura = 3 } };

    std.debug.print("Área do círculo: {d:.2}\n", .{c.area()});
    std.debug.print("Área do retângulo: {d:.2}\n", .{r.area()});
}

Switch Exaustivo

fn descrever(forma: Forma) []const u8 {
    return switch (forma) {
        .circulo => "É um círculo",
        .retangulo => "É um retângulo",
        .triangulo => "É um triângulo",
        // Se você esquecer uma variante, o compilador emite ERRO
    };
}

Tagged Union com Enum Explícito

const TipoEvento = enum {
    teclado,
    mouse,
    janela,
};

const Evento = union(TipoEvento) {
    teclado: struct { tecla: u32, pressionada: bool },
    mouse: struct { x: i32, y: i32, botao: u8 },
    janela: struct { largura: u32, altura: u32 },
};

fn processarEvento(evento: Evento) void {
    switch (evento) {
        .teclado => |k| {
            if (k.pressionada) {
                std.debug.print("Tecla {} pressionada\n", .{k.tecla});
            }
        },
        .mouse => |m| {
            std.debug.print("Mouse em ({}, {})\n", .{ m.x, m.y });
        },
        .janela => |j| {
            std.debug.print("Janela: {}x{}\n", .{ j.largura, j.altura });
        },
    }
}

Acessando a Tag

const evento = Evento{ .mouse = .{ .x = 100, .y = 200, .botao = 1 } };

// Acessar a tag como enum
const tag = std.meta.activeTag(evento);
if (tag == .mouse) {
    std.debug.print("É evento de mouse!\n", .{});
}

Armadilhas Comuns

  • Acessar campo inativo: Tentar ler .circulo quando a variante ativa é .retangulo causa panic em Debug. Use sempre switch.
  • Esquecer variantes no switch: Sem else, o compilador exige exaustividade. Isso é uma vantagem — use-a.
  • Confundir com union simples: union(enum) é tagged; union sem tag é insegura como em C.
  • Tamanho: O tamanho da tagged union é o do maior campo + tag. Se um campo for muito maior que os outros, considere usar um ponteiro.

Casos de Uso

Tagged unions aparecem em diversas situações onde um valor pode ter diferentes formas:

  • Tokens de parser/lexer: Um token pode ser um número, uma string, um identificador ou um símbolo.
  • Mensagens de sistema: Eventos de UI, comandos de protocolo, respostas de API.
  • Tipos algébricos: Implementar Result<T, E> ou Option<T> do estilo funcional.
  • Nós de AST: Cada nó de uma árvore sintática abstrata tem um tipo diferente de dado.
const std = @import("std");

const Token = union(enum) {
    numero: i64,
    texto: []const u8,
    identificador: []const u8,
    simbolo: u8,
    fim_arquivo,

    pub fn format(self: Token, writer: anytype) !void {
        switch (self) {
            .numero => |n| try writer.print("Num({})", .{n}),
            .texto => |s| try writer.print("Str({s})", .{s}),
            .identificador => |id| try writer.print("Id({s})", .{id}),
            .simbolo => |c| try writer.print("Sym({c})", .{c}),
            .fim_arquivo => try writer.print("EOF", .{}),
        }
    }
};

Comparação com Outras Linguagens

Em C, unions não sabem qual campo está ativo — cabe ao programador manter um campo de “tipo” manualmente, o que é error-prone. Em Rust, o equivalente é o enum (que em Rust pode carregar dados, ao contrário de C). Em C++17, há std::variant. Zig oferece a mesma segurança que Rust e C++, com sintaxe mais simples e sem overhead de runtime além do tamanho da tag.

A principal vantagem sobre C é que o compilador de Zig emite erro de compilação se você esquecer um caso no switch, eliminando bugs silenciosos que surgem quando novas variantes são adicionadas.

Termos Relacionados

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.