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
- Introdução aos Tipos Compostos
- Structs em Zig
- Enums em Zig
- Unions em Zig
- Tipos Especiais e Padrões
- Comparação com C
- Exemplos Práticos Completos
- Melhores Práticas
- FAQ
- 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
| Tipo | Uso Principal | Analogia em C |
|---|---|---|
| Struct | Agrupar campos relacionados | struct |
| Enum | Definir conjunto de valores nomeados | enum |
| Union | Armazenar diferentes tipos no mesmo espaço | union |
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
constevar
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
| Aspecto | Zig | C |
|---|---|---|
| Struct | struct { x: i32 } | struct { int x; } |
| Enum | enum { a, b } com tipo opcional | Sempre int |
| Union | union { x: i32 } | union { int x; } |
| Tagged Union | Nativo com union(enum) | Não existe |
| Optional | ?T built-in | Requer ponteiros |
| Error handling | !T built-in | Códigos de erro |
| Métodos | Nativo em structs/enums | Requer funções separadas |
| Type safety | Strong | Weak (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
- Gerenciamento de Memória em Zig — Aprofunde em allocators e como structs são alocadas
- Comptime em Zig — Aprenda metaprogramação para gerar structs dinamicamente
- Strings e Arrays em Zig — Fundamentos de coleções de dados
- Tratamento de Erros em Zig — Domine error unions e error sets
- 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
| Conceito | Uso Principal | Exemplo |
|---|---|---|
| Struct | Agrupar dados relacionados | Ponto{ .x = 1, .y = 2 } |
| Enum | Conjunto de valores nomeados | Status.ativo |
| Union | Diferentes tipos no mesmo espaço | Dado{ .inteiro = 42 } |
| Tagged Union | Union segura com discriminação | union(enum) { a: i32 } |
| Optional | Valor ou nulo | ?i32 |
| Error Union | Valor 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