@typeInfo em Zig — Referência e Exemplos

@typeInfo em Zig

O @typeInfo é um dos builtins mais poderosos do Zig para metaprogramação. Ele permite inspecionar qualquer tipo em tempo de compilação, retornando uma union detalhada com todas as informações sobre a estrutura daquele tipo. É fundamental para escrever código genérico e bibliotecas que operam sobre tipos arbitrários.

Sintaxe

@typeInfo(comptime T: type) std.builtin.Type

O que faz

O @typeInfo recebe um tipo como parâmetro e retorna uma instância da union std.builtin.Type, que contém informações detalhadas sobre o tipo fornecido. Essa union possui variantes para cada categoria de tipo em Zig: inteiros, floats, ponteiros, arrays, structs, enums, unions, funções, entre outros.

Com essas informações, é possível escrever funções genéricas que se comportam de forma diferente dependendo das propriedades do tipo recebido, implementar serialização automática, validação de tipos e muito mais.

Parâmetros

  • T (type, comptime): O tipo a ser inspecionado. Deve ser conhecido em tempo de compilação. Pode ser qualquer tipo válido em Zig, incluindo tipos primitivos, structs, enums, unions, ponteiros, arrays e tipos de função.

Valor de retorno

Retorna uma instância de std.builtin.Type, que é uma tagged union com variantes como:

  • .int — Informações sobre tipos inteiros (bits, sinalização)
  • .float — Informações sobre tipos de ponto flutuante
  • .pointer — Informações sobre ponteiros (alinhamento, sentinela, tamanho)
  • .array — Informações sobre arrays (comprimento, tipo filho)
  • .@"struct" — Informações sobre structs (campos, declarações)
  • .@"enum" — Informações sobre enums (campos, tipo de tag)
  • .@"union" — Informações sobre unions (campos, tipo de tag)
  • .@"fn" — Informações sobre funções (parâmetros, retorno)
  • E muitas outras variantes.

Exemplos práticos

Exemplo 1: Verificando a categoria de um tipo

const std = @import("std");

fn descreverTipo(comptime T: type) []const u8 {
    const info = @typeInfo(T);
    return switch (info) {
        .int => "tipo inteiro",
        .float => "tipo ponto flutuante",
        .pointer => "tipo ponteiro",
        .array => "tipo array",
        .@"struct" => "tipo struct",
        .@"enum" => "tipo enum",
        .bool => "tipo booleano",
        else => "outro tipo",
    };
}

test "descrever tipos" {
    try std.testing.expectEqualStrings("tipo inteiro", descreverTipo(i32));
    try std.testing.expectEqualStrings("tipo ponto flutuante", descreverTipo(f64));
    try std.testing.expectEqualStrings("tipo booleano", descreverTipo(bool));
}

Exemplo 2: Inspecionando campos de uma struct

const std = @import("std");

const Pessoa = struct {
    nome: []const u8,
    idade: u32,
    ativo: bool,
};

fn listarCampos(comptime T: type) void {
    const info = @typeInfo(T);
    switch (info) {
        .@"struct" => |s| {
            inline for (s.fields) |campo| {
                std.debug.print("Campo: {s}, Tipo: {s}\n", .{
                    campo.name,
                    @typeName(campo.type),
                });
            }
        },
        else => @compileError("Esperava uma struct"),
    }
}

test "listar campos de Pessoa" {
    listarCampos(Pessoa);
    // Saída:
    // Campo: nome, Tipo: []const u8
    // Campo: idade, Tipo: u32
    // Campo: ativo, Tipo: bool
}

Exemplo 3: Serialização genérica simples

const std = @import("std");

fn toJson(comptime T: type, valor: T, writer: anytype) !void {
    const info = @typeInfo(T);
    switch (info) {
        .int, .comptime_int => try writer.print("{}", .{valor}),
        .float, .comptime_float => try writer.print("{d}", .{valor}),
        .bool => try writer.print("{}", .{valor}),
        .pointer => |ptr| {
            if (ptr.size == .slice and ptr.child == u8) {
                try writer.print("\"{s}\"", .{valor});
            }
        },
        .@"struct" => |s| {
            try writer.writeByte('{');
            inline for (s.fields, 0..) |campo, i| {
                if (i > 0) try writer.writeAll(", ");
                try writer.print("\"{s}\": ", .{campo.name});
                try toJson(campo.type, @field(valor, campo.name), writer);
            }
            try writer.writeByte('}');
        },
        else => try writer.writeAll("null"),
    }
}

Casos de uso comuns

  1. Serialização/Deserialização genérica: Iterar sobre campos de structs para converter automaticamente de/para JSON, MessagePack ou outros formatos.

  2. Validação de tipos genéricos: Em funções genéricas, verificar se o tipo fornecido possui as propriedades necessárias (campos específicos, métodos, etc.).

  3. Geração automática de código: Criar implementações de funções baseadas na estrutura de tipos, como toString(), comparação, hash, etc.

  4. Debug e logging: Imprimir informações detalhadas sobre tipos para depuração em tempo de compilação.

  5. Frameworks e bibliotecas: Construir abstrações que funcionam com qualquer tipo, como ORMs, frameworks web e sistemas de eventos.

Trabalhando com enums via @typeInfo

Inspecionar enums é um dos usos mais comuns de @typeInfo. O campo .@"enum" fornece a lista de variantes, o tipo de tag e se o enum é exaustivo:

const std = @import("std");

fn listarVariantes(comptime E: type) void {
    const info = @typeInfo(E);
    switch (info) {
        .@"enum" => |e| {
            std.debug.print("Enum com {} variantes (tag: {s}):\n", .{
                e.fields.len,
                @typeName(e.tag_type),
            });
            inline for (e.fields) |campo| {
                std.debug.print("  .{s} = {}\n", .{ campo.name, campo.value });
            }
        },
        else => @compileError("Esperava enum"),
    }
}

const Direcao = enum(u8) { norte = 0, sul = 1, leste = 2, oeste = 3 };

test "listar enum" {
    listarVariantes(Direcao);
}

Verificação de interfaces em comptime

Um padrão poderoso é usar @typeInfo para verificar se um tipo implementa uma “interface” — um conjunto de métodos e campos esperados:

fn verificarInterface(comptime T: type) void {
    const info = @typeInfo(T);
    if (info != .@"struct") {
        @compileError(@typeName(T) ++ " deve ser uma struct");
    }

    // Verificar se tem método `format`
    if (!@hasDecl(T, "format")) {
        @compileError(@typeName(T) ++ " deve ter método `format`");
    }

    // Verificar campo obrigatório
    if (!@hasField(T, "id")) {
        @compileError(@typeName(T) ++ " deve ter campo `id`");
    }
}

Essa técnica é usada extensivamente na biblioteca padrão do Zig para garantir contratos de tipos em tempo de compilação, sem a necessidade de traits ou interfaces explícitas.

Iteração sobre campos de struct em comptime

O padrão inline for com @typeInfo é essencial para metaprogramação genérica:

const std = @import("std");

// Comparação automática de structs campo a campo
fn eql(comptime T: type, a: T, b: T) bool {
    const info = @typeInfo(T);
    if (info != .@"struct") @compileError("Apenas structs");

    inline for (info.@"struct".fields) |campo| {
        const va = @field(a, campo.name);
        const vb = @field(b, campo.name);
        if (va != vb) return false;
    }
    return true;
}

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

test "comparação genérica" {
    const p1 = Ponto{ .x = 1.0, .y = 2.0 };
    const p2 = Ponto{ .x = 1.0, .y = 2.0 };
    const p3 = Ponto{ .x = 1.0, .y = 3.0 };

    try std.testing.expect(eql(Ponto, p1, p2));
    try std.testing.expect(!eql(Ponto, p1, p3));
}

Perguntas Frequentes

P: @typeInfo pode causar erros de compilação?

R: Não por si só — mas o código que usa o resultado pode. Por exemplo, acessar .@"struct" em um tipo que não é struct causaria um erro em runtime de comptime (que é um erro de compilação). Use switch para tratar as variantes corretamente.

P: Qual é a diferença entre @typeInfo e @hasField/@hasDecl?

R: @hasField e @hasDecl são verificações pontuais (sim/não). @typeInfo fornece o conjunto completo de informações sobre o tipo, permitindo inspeção profunda, iteração sobre campos e verificações complexas. Para verificações simples, prefira @hasField/@hasDecl por clareza.

P: Posso usar @typeInfo para inspecionar tipos de erro (anyerror, !T)?

R: Sim. @typeInfo(anyerror) retorna .error_set, e @typeInfo(!T) retorna .error_union com os campos .error_set e .payload. Isso permite metaprogramação sobre conjuntos de erros.

Builtins relacionados

  • @typeName — Obtém o nome de um tipo como string
  • @TypeOf — Obtém o tipo de uma expressão
  • @hasField — Verifica se um tipo possui determinado campo
  • @hasDecl — Verifica se um tipo possui determinada declaração
  • @field — Acessa campo de struct por nome

Tutoriais relacionados

Continue aprendendo Zig

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