Structs, Enums e Unions em Zig: Guia Completo

Structs, Enums e Unions em Zig: Guia Completo

Structs, enums e unions são os tipos compostos fundamentais de Zig. Eles formam a base para modelar dados complexos e são essenciais para qualquer programa não-trivial. Se você vem de C, vai reconhecer esses conceitos — mas Zig adiciona recursos modernos que tornam o código mais seguro e expressivo.

Neste guia completo, você vai aprender tudo sobre tipos compostos em Zig: desde os conceitos básicos até padrões avançados usados em código de produção.

Pré-requisitos: Conhecimento básico de Zig (variáveis, funções, tipos primitivos). Se você é novo no Zig, comece com nosso guia de instalação e introdução à linguagem.


Índice

  1. Introdução aos Tipos Compostos
  2. Structs em Zig
  3. Enums em Zig
  4. Unions em Zig
  5. Tipos Especiais e Padrões
  6. Comparação com C
  7. Exemplos Práticos Completos
  8. Melhores Práticas
  9. FAQ
  10. Próximos Passos

Introdução aos Tipos Compostos

Em Zig, tipos compostos permitem agrupar dados relacionados em uma única estrutura. Eles são zero-cost abstractions — não adicionam overhead em runtime comparado a acessar os dados manualmente.

Os Três Tipos Compostos

TipoUso PrincipalAnalogia em C
StructAgrupar campos relacionadosstruct
EnumDefinir conjunto de valores nomeadosenum
UnionArmazenar diferentes tipos no mesmo espaçounion

Por Que São Importantes?

// ❌ Sem struct: dados desorganizados
const nome = "Alice";
const idade = 30;
const email = "alice@email.com";

// ✅ Com struct: dados coesos
const Usuario = struct {
    nome: []const u8,
    idade: u32,
    email: []const u8,
};

Structs, enums e unions trabalham juntos para criar modelos de dados robustos e type-safe.


Structs em Zig

Structs são coleções nomeadas de campos. São o tipo composto mais comum em Zig.

Declaração Básica

const Ponto = struct {
    x: f64,
    y: f64,
};

const Retangulo = struct {
    origem: Ponto,
    largura: f64,
    altura: f64,
};

Structs podem conter:

  • Campos de qualquer tipo (incluindo outros structs)
  • Métodos (funções associadas)
  • Declarações const e var

Inicialização

const std = @import("std");

pub fn main() void {
    // Inicialização com valores nomeados (recomendado)
    const p1 = Ponto{
        .x = 10.5,
        .y = 20.3,
    };
    
    // Inicialização com valores posicionais
    const p2 = Ponto{ 5.0, 8.0 };
    
    // Inicialização parcial com valores padrão (comptime)
    const Retangulo = struct {
        origem: Ponto = Ponto{ .x = 0, .y = 0 },
        largura: f64,
        altura: f64,
    };
    
    const ret = Retangulo{
        .largura = 100,
        .altura = 50,
        // origem usa valor padrão
    };
    
    std.debug.print("Ponto 1: ({d}, {d})\n", .{ p1.x, p1.y });
}

Acesso a Campos

const p = Ponto{ .x = 10, .y = 20 };

// Acesso direto
const x = p.x;
const y = p.y;

// Modificação (requer variável mutável)
var p_mut = Ponto{ .x = 0, .y = 0 };
p_mut.x = 15;
p_mut.y = 25;

Métodos em Structs

Structs podem ter métodos — funções que operam na struct:

const Circulo = struct {
    centro: Ponto,
    raio: f64,
    
    // Método que recebe self por valor (imutável)
    pub fn area(self: Circulo) f64 {
        return std.math.pi * self.raio * self.raio;
    }
    
    // Método que recebe self por referência mutável
    pub fn escalar(self: *Circulo, fator: f64) void {
        self.raio *= fator;
    }
    
    // Método estático (não recebe self)
    pub fn unitario() Circulo {
        return Circulo{
            .centro = Ponto{ .x = 0, .y = 0 },
            .raio = 1.0,
        };
    }
};

pub fn main() void {
    var c = Circulo{
        .centro = Ponto{ .x = 0, .y = 0 },
        .raio = 5.0,
    };
    
    // Chamada de método
    std.debug.print("Área: {d}\n", .{c.area()});
    
    // Modificação via método
    c.escalar(2.0);
    std.debug.print("Novo raio: {d}\n", .{c.raio});
    
    // Método estático
    const unit = Circulo.unitario();
}

Self em Métodos

Zig não tem self implícito como Python. Você declara explicitamente:

const MeuStruct = struct {
    valor: i32,
    
    // self por valor (cópia)
    pub fn getValorCopia(self: MeuStruct) i32 {
        return self.valor;
    }
    
    // self por referência constante
    pub fn getValorRef(self: *const MeuStruct) i32 {
        return self.valor;
    }
    
    // self por referência mutável
    pub fn setValor(self: *MeuStruct, novo: i32) void {
        self.valor = novo;
    }
};

Recomendação: Use *const Self para métodos de leitura, *Self para métodos de modificação.

Structs Anônimos

Structs podem ser declarados sem nome (anônimos):

// Struct anônimo como tipo de retorno
fn criarPessoa(nome: []const u8, idade: u32) struct { nome: []const u8, idade: u32 } {
    return .{ .nome = nome, .idade = idade };
}

// Uso
const pessoa = criarPessoa("Alice", 30);

Structs anônimos são úteis para tipos temporários ou retornos de funções.

Structs com packed

Structs packed garantem layout específico na memória (útil para binary protocols):

const HeaderPacote = packed struct {
    versao: u4,
    flags: u4,
    tamanho: u16,
    checksum: u32,
};

// Tamanho garantido: exatamente 8 bytes
comptime {
    std.debug.assert(@sizeOf(HeaderPacote) == 8);
}

Atenção: Structs packed têm restrições — campos não podem ser ponteiros para self.

Structs com extern

Structs extern têm layout compatível com C:

const CoordenadaC = extern struct {
    x: c_int,
    y: c_int,
};

// Pode ser passado diretamente para funções C

Enums em Zig

Enums definem um tipo que pode ter um de vários valores nomeados.

Declaração Básica

const StatusPedido = enum {
    pendente,
    processando,
    enviado,
    entregue,
    cancelado,
};

const Cor = enum {
    vermelho,
    verde,
    azul,
};

Por padrão, enums em Zig usam usize como tipo subjacente (0, 1, 2, …).

Enums com Tipo Específico

// Enum com tipo subjacente específico
const CodigoErro = enum(u8) {
    sucesso = 0,
    arquivo_nao_encontrado = 1,
    permissao_negada = 2,
    memoria_insuficiente = 3,
    timeout = 4,
};

// Tamanho explícito
comptime {
    std.debug.assert(@sizeOf(CodigoErro) == 1); // u8 = 1 byte
}

Valores Personalizados

const FlagsPermissao = enum(u8) {
    nenhuma = 0,
    leitura = 1 << 0,    // 1
    escrita = 1 << 1,    // 2
    execucao = 1 << 2,   // 4
    todas = 0b111,       // 7
};

Métodos em Enums

Enums também podem ter métodos:

const Status = enum {
    ativo,
    inativo,
    suspenso,
    
    pub fn podeAcessar(self: Status) bool {
        return self == .ativo;
    }
    
    pub fn descricao(self: Status) []const u8 {
        return switch (self) {
            .ativo => "Usuário ativo",
            .inativo => "Usuário inativo",
            .suspenso => "Usuário suspenso",
        };
    }
};

pub fn main() void {
    const status = Status.ativo;
    std.debug.print("Pode acessar? {s}\n", .{if (status.podeAcessar()) "sim" else "não"});
    std.debug.print("Descrição: {s}\n", .{status.descricao()});
}

Conversão para/de Inteiro

const codigo = @intFromEnum(CodigoErro.permissao_negada); // 2
const erro = @enumFromInt(CodigoErro, 2);                 // .permissao_negada

std.meta.Tag

Para obter o tipo subjacente de um enum:

const TipoSubjacente = @typeInfo(Status).Enum.tag_type; // usize

Unions em Zig

Unions permitem armazenar diferentes tipos no mesmo espaço de memória. Zig oferece dois tipos: untagged (inseguro) e tagged (seguro).

Union Untagged (Inseguro)

Similar a unions em C — você é responsável por rastrear qual campo está ativo:

const Dado = union {
    inteiro: i32,
    flutuante: f64,
    texto: []const u8,
};

pub fn main() void {
    var dado: Dado = undefined;
    
    // Armazena inteiro
    dado.inteiro = 42;
    std.debug.print("Inteiro: {d}\n", .{dado.inteiro});
    
    // Agora armazena float (sobrescreve inteiro)
    dado.flutuante = 3.14;
    std.debug.print("Float: {d}\n", .{dado.flutuante});
    
    // ⚠️ Acessar campo errado é comportamento indefinido!
    // std.debug.print("{d}", .{dado.inteiro}); // PERIGOSO!
}

Union Tagged (Seguro)

Tagged unions combinam uma union com um enum — o enum indica qual campo está ativo:

const Valor = union(enum) {
    inteiro: i32,
    flutuante: f64,
    texto: []const u8,
    nulo,
};

pub fn main() void {
    const v1 = Valor{ .inteiro = 42 };
    const v2 = Valor{ .flutuante = 3.14 };
    const v3 = Valor{ .texto = "Olá" };
    const v4 = Valor.nulo;
    
    // Switch seguro — o compilador garante que todos os casos são tratados
    switch (v1) {
        .inteiro => |i| std.debug.print("Inteiro: {d}\n", .{i}),
        .flutuante => |f| std.debug.print("Float: {d}\n", .{f}),
        .texto => |t| std.debug.print("Texto: {s}\n", .{t}),
        .nulo => std.debug.print("Nulo\n"),
    }
}

A sintaxe union(enum) cria automaticamente um enum implícito. Você também pode usar um enum explícito:

const TipoValor = enum {
    inteiro,
    flutuante,
    texto,
};

const ValorExplicito = union(TipoValor) {
    inteiro: i32,
    flutuante: f64,
    texto: []const u8,
};

Captura por Referência

No switch, você pode capturar o valor por referência para modificação:

var valor = Valor{ .inteiro = 10 };

switch (valor) {
    .inteiro => |*i| i.* += 1,  // Modifica o valor
    else => {},
}

std.debug.print("Valor: {d}\n", .{valor.inteiro}); // 11

Union com packed e extern

// Layout compatível com C
const DadoC = extern union {
    inteiro: c_int,
    flutuante: f32,
};

// Layout packed
const DadoPacked = packed union {
    bits: u32,
    float: f32,
};

Tipos Especiais e Padrões

Optional Types (?T)

Optional é um tipo built-in que representa “valor ou nulo”:

// ?i32 pode ser i32 ou null
const talvezNumero: ?i32 = 42;
const talvezNulo: ?i32 = null;

// Desempacotamento seguro
if (talvezNumero) |numero| {
    std.debug.print("Valor: {d}\n", .{numero});
} else {
    std.debug.print("É nulo\n");
}

// Operador orelse (valor padrão)
const valor = talvezNulo orelse 0; // 0

// Orelse com retorno
const valorOuErro = talvezNulo orelse return error.ValorNulo;

Error Union (!T)

Error union representa “valor ou erro”:

// !i32 pode ser i32 ou um erro
fn dividir(a: i32, b: i32) !i32 {
    if (b == 0) return error.DivisaPorZero;
    return @divTrunc(a, b);
}

// Try propaga erros
const resultado = try dividir(10, 2);

// Catch trata erros
const seguro = dividir(10, 0) catch |err| {
    std.debug.print("Erro: {}\n", .{err});
    0 // valor padrão
};

Anyerror

anyerror é o tipo de todos os erros:

const MeuErro = error{ NaoEncontrado, Timeout };
const OutroErro = error{ PermissaoNegada };

// anyerror pode ser qualquer erro
fn podeFalhar() anyerror!void {
    return error.NaoEncontrado;
}

Error Sets

Conjuntos de erros podem ser combinados:

const ErroIO = error{ ArquivoNaoEncontrado, PermissaoNegada };
const ErroRede = error{ Timeout, ConexaoRecusada };

// União de erros
const ErroApp = ErroIO || ErroRede;

fn operacaoComplexa() ErroApp!void {
    // Pode retornar qualquer erro de ErroIO ou ErroRede
}

Type Inference com .

Zig pode inferir o tipo em muitos contextos:

const Status = enum { ativo, inativo };

// Inferido como Status.ativo
const s: Status = .ativo;

const Valor = union(enum) { numero: i32, texto: []const u8 };

// Inferido como Valor{ .numero = 42 }
const v: Valor = .{ .numero = 42 };

Comparação com C

AspectoZigC
Structstruct { x: i32 }struct { int x; }
Enumenum { a, b } com tipo opcionalSempre int
Unionunion { x: i32 }union { int x; }
Tagged UnionNativo com union(enum)Não existe
Optional?T built-inRequer ponteiros
Error handling!T built-inCódigos de erro
MétodosNativo em structs/enumsRequer funções separadas
Type safetyStrongWeak (casts implícitos)

Exemplo Comparativo

C:

// Struct simples
struct Ponto {
    double x;
    double y;
};

// Enum
enum Status { ATIVO, INATIVO };

// Union insegura
union Dado {
    int i;
    double f;
};

// Sem tagged unions nativamente
// Sem optional types
// Sem error unions

Zig:

// Struct com métodos
const Ponto = struct {
    x: f64,
    y: f64,
    
    pub fn distancia(self: Ponto, outro: Ponto) f64 {
        const dx = self.x - outro.x;
        const dy = self.y - outro.y;
        return std.math.sqrt(dx * dx + dy * dy);
    }
};

// Enum com tipo específico
const Status = enum(u8) { ativo, inativo };

// Union tagged (segura)
const Dado = union(enum) {
    inteiro: i32,
    flutuante: f64,
};

// Optional e error unions nativos
const talvezValor: ?i32 = null;
const resultado: !i32 = calcular();

Exemplos Práticos Completos

Exemplo 1: Sistema de Configuração

const std = @import("std");

const Config = struct {
    nome: []const u8,
    versao: []const u8,
    debug: bool = false,
    portas: struct {
        http: u16 = 8080,
        https: u16 = 8443,
    } = .{},
};

const Ambiente = enum {
    desenvolvimento,
    homologacao,
    producao,
};

fn criarConfig(ambiente: Ambiente) Config {
    return switch (ambiente) {
        .desenvolvimento => Config{
            .nome = "MeuApp",
            .versao = "dev",
            .debug = true,
            .portas = .{ .http = 3000, .https = 3443 },
        },
        .homologacao => Config{
            .nome = "MeuApp",
            .versao = "beta",
            .debug = true,
        },
        .producao => Config{
            .nome = "MeuApp",
            .versao = "1.0.0",
            .debug = false,
        },
    };
}

pub fn main() void {
    const config = criarConfig(.desenvolvimento);
    std.debug.print("App: {s} v{s}\n", .{ config.nome, config.versao });
    std.debug.print("Porta HTTP: {d}\n", .{config.portas.http});
}

Exemplo 2: AST (Abstract Syntax Tree)

const Expr = union(enum) {
    numero: i64,
    variavel: []const u8,
    binaria: struct {
        op: Operador,
        esq: *const Expr,
        dir: *const Expr,
    },
    chamada: struct {
        funcao: []const u8,
        args: []const Expr,
    },
    
    const Operador = enum {
        soma,
        subtracao,
        multiplicacao,
        divisao,
    };
    
    pub fn avaliar(self: Expr) !i64 {
        return switch (self) {
            .numero => |n| n,
            .variavel => error.VariavelNaoSuportada,
            .binaria => |b| {
                const esq = try b.esq.avaliar();
                const dir = try b.dir.avaliar();
                return switch (b.op) {
                    .soma => esq + dir,
                    .subtracao => esq - dir,
                    .multiplicacao => esq * dir,
                    .divisao => @divTrunc(esq, dir),
                };
            },
            .chamada => error.ChamadaNaoSuportada,
        };
    }
};

pub fn main() !void {
    const dois = Expr{ .numero = 2 };
    const tres = Expr{ .numero = 3 };
    
    const soma = Expr{
        .binaria = .{
            .op = .soma,
            .esq = &dois,
            .dir = &tres,
        },
    };
    
    const resultado = try soma.avaliar();
    std.debug.print("2 + 3 = {d}\n", .{resultado});
}

Exemplo 3: Resultado de Operação

const Resultado = union(enum) {
    sucesso: []const u8,
    erro: Erro,
    
    const Erro = struct {
        codigo: u32,
        mensagem: []const u8,
    };
    
    pub fn estaOk(self: Resultado) bool {
        return self == .sucesso;
    }
    
    pub fn getMensagem(self: Resultado) []const u8 {
        return switch (self) {
            .sucesso => |s| s,
            .erro => |e| e.mensagem,
        };
    }
};

fn operacaoArquivos() Resultado {
    // Simula operação
    const sucesso = false;
    
    if (sucesso) {
        return .{ .sucesso = "Arquivo processado com sucesso" };
    } else {
        return .{ .erro = .{
            .codigo = 404,
            .mensagem = "Arquivo não encontrado",
        } };
    }
}

pub fn main() void {
    const resultado = operacaoArquivos();
    
    if (resultado.estaOk()) {
        std.debug.print("✅ {s}\n", .{resultado.getMensagem()});
    } else {
        std.debug.print("❌ Erro: {s}\n", .{resultado.getMensagem()});
    }
}

Melhores Práticas

1. Use Structs para Agrupar Dados Relacionados

// ✅ Bom
const Usuario = struct {
    id: u64,
    nome: []const u8,
    email: []const u8,
    criado_em: i64,
};

// ❌ Ruim: dados soltos
const usuario_id: u64 = 1;
const usuario_nome: []const u8 = "Alice";
const usuario_email: []const u8 = "alice@email.com";

2. Prefira Tagged Unions

// ✅ Seguro: tagged union
const Resultado = union(enum) {
    sucesso: Dados,
    erro: MensagemErro,
};

// ❌ Inseguro: untagged union (use apenas quando necessário)
const ResultadoInseguro = union {
    sucesso: Dados,
    erro: MensagemErro,
};

3. Use Valores Padrão

const Config = struct {
    timeout_ms: u32 = 5000,      // valor padrão
    max_retries: u32 = 3,        // valor padrão
    log_level: LogLevel = .info, // valor padrão
};

// Inicialização simples
const config = Config{ .timeout_ms = 10000 };
// max_retries e log_level usam defaults

4. Implemente Métodos Úteis

const Vetor2D = struct {
    x: f64,
    y: f64,
    
    // Construtores
    pub fn zero() Vetor2D {
        return .{ .x = 0, .y = 0 };
    }
    
    pub fn new(x: f64, y: f64) Vetor2D {
        return .{ .x = x, .y = y };
    }
    
    // Operações
    pub fn add(self: Vetor2D, outro: Vetor2D) Vetor2D {
        return .{
            .x = self.x + outro.x,
            .y = self.y + outro.y,
        };
    }
    
    pub fn magnitude(self: Vetor2D) f64 {
        return std.math.sqrt(self.x * self.x + self.y * self.y);
    }
};

5. Documente com Comentários

/// Representa um usuário do sistema.
/// Campos obrigatórios: id, nome
/// Campos opcionais: avatar, bio
const Usuario = struct {
    id: u64,                // ID único do usuário
    nome: []const u8,       // Nome de exibição
    email: []const u8,      // Email para login
    ativo: bool = true,     // Status da conta
};

6. Use extern para Interoperabilidade com C

// Sempre use extern struct quando for passar para C
const PontoC = extern struct {
    x: f64,
    y: f64,
};

// Função exportada para C
export fn calcularDistancia(a: PontoC, b: PontoC) f64 {
    // ...
}

FAQ

Qual a diferença entre struct e union?

Struct: Todos os campos existem simultaneamente. Tamanho = soma dos campos.

Union: Apenas um campo existe por vez. Tamanho = maior campo.

const S = struct { a: i32, b: f64 };  // 16 bytes
const U = union  { a: i32, b: f64 };  // 8 bytes

Como criar um construtor em Zig?

Structs não têm construtores especiais. Use funções estáticas:

const Ponto = struct {
    x: f64,
    y: f64,
    
    pub fn new(x: f64, y: f64) Ponto {
        return .{ .x = x, .y = y };
    }
};

const p = Ponto.new(10, 20);

Posso ter herança em Zig?

Não. Zig não tem herança de classes. Use composição:

const Animal = struct {
    nome: []const u8,
};

const Cachorro = struct {
    animal: Animal,  // composição
    raca: []const u8,
};

Como fazer pattern matching em enums?

Use switch:

const status = Status.ativo;

const mensagem = switch (status) {
    .ativo => "Online",
    .inativo => "Offline",
};

Union tagged vs optional — quando usar cada?

  • Optional (?T): “tem valor ou não tem”
  • Union tagged: “tem valor do tipo A, OU tipo B, OU tipo C”
const talvezNumero: ?i64 = null;              // simples
const valor: union(enum) { i: i64, f: f64 };  // múltiplos tipos

Como verificar o tipo atual de uma tagged union?

const valor = Valor{ .inteiro = 42 };

if (valor == .inteiro) {
    std.debug.print("É um inteiro: {d}\n", .{valor.inteiro});
}

Posso modificar uma tagged union?

Sim, mas precisa ser mutável:

var valor = Valor{ .inteiro = 10 };
valor = Valor{ .flutuante = 3.14 };  // OK

Próximos Passos

Agora que você domina structs, enums e unions em Zig, continue seu aprendizado:

Conteúdo Relacionado

  1. Gerenciamento de Memória em Zig — Aprofunde em allocators e como structs são alocadas
  2. Comptime em Zig — Aprenda metaprogramação para gerar structs dinamicamente
  3. Strings e Arrays em Zig — Fundamentos de coleções de dados
  4. Tratamento de Erros em Zig — Domine error unions e error sets
  5. Zig para Programadores C — Compare structs/unions com C

Pratique

  • Implemente uma árvore binária usando structs e tagged unions
  • Crie um parser simples usando tagged unions para tipos de tokens
  • Modele um sistema de estados (máquina de estados) com enums

Recursos Adicionais


Resumo

ConceitoUso PrincipalExemplo
StructAgrupar dados relacionadosPonto{ .x = 1, .y = 2 }
EnumConjunto de valores nomeadosStatus.ativo
UnionDiferentes tipos no mesmo espaçoDado{ .inteiro = 42 }
Tagged UnionUnion segura com discriminaçãounion(enum) { a: i32 }
OptionalValor ou nulo?i32
Error UnionValor ou erro!i32

Structs, enums e unions são os blocos de construção para modelar dados em Zig. Combinados com optional types e error unions, eles formam um sistema de tipos expressivo e type-safe que ajuda a prevenir bugs em tempo de compilação.


Escrito por Camila para ZigLang Brasil. Última atualização: 10 de fevereiro de 2026.

Achou algum erro? Tem sugestões? Contribua no GitHub

Continue aprendendo Zig

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