hasField em Zig (@hasField)
O hasField em Zig é o builtin @hasField: ele responde, em tempo de compilação, se um tipo tem um campo com determinado nome. É a forma idiomática de escrever código genérico e duck typing sem quebrar a compilação quando o campo não existe.
const Pessoa = struct { nome: []const u8, idade: u32 };
comptime {
_ = @hasField(Pessoa, "nome"); // true
_ = @hasField(Pessoa, "cpf"); // false
}
Em resumo: @hasField(T, "campo") retorna bool em comptime. Use-o antes de @field em funções genéricas, para mapear DTOs ou para impor contratos com @compileError.
Sintaxe
@hasField(comptime T: type, comptime nome: []const u8) bool
O que faz
O @hasField consulta a definição do tipo fornecido e verifica se ele contém um campo com o nome especificado. A verificação ocorre só em tempo de compilação — sem custo em runtime. Retorna true se o campo existir e false caso contrário.
Parâmetros
- T (
type, comptime): O tipo a ser inspecionado. Deve ser uma struct, union ou enum. - nome (
[]const u8, comptime): O nome do campo a ser verificado, como string.
Valor de retorno
Retorna bool (comptime) — true se o campo existir no tipo, false caso contrário.
Exemplos práticos
Exemplo 1: Verificação básica de campos
const std = @import("std");
const Pessoa = struct {
nome: []const u8,
idade: u32,
email: []const u8,
};
test "verificar existência de campos" {
try std.testing.expect(@hasField(Pessoa, "nome") == true);
try std.testing.expect(@hasField(Pessoa, "idade") == true);
try std.testing.expect(@hasField(Pessoa, "email") == true);
try std.testing.expect(@hasField(Pessoa, "telefone") == false);
try std.testing.expect(@hasField(Pessoa, "cpf") == false);
}
Exemplo 2: Mapeamento adaptativo entre tipos
const std = @import("std");
fn copiarCamposComuns(comptime Destino: type, comptime Origem: type, origem: Origem) Destino {
var resultado: Destino = undefined;
const campos_destino = @typeInfo(Destino).@"struct".fields;
inline for (campos_destino) |campo| {
if (@hasField(Origem, campo.name)) {
@field(resultado, campo.name) = @field(origem, campo.name);
} else {
// Usar valor padrão se disponível, ou zero
if (campo.default_value) |default_ptr| {
const ptr: *const campo.type = @ptrCast(@alignCast(default_ptr));
@field(resultado, campo.name) = ptr.*;
} else {
@field(resultado, campo.name) = std.mem.zeroes(campo.type);
}
}
}
return resultado;
}
const UsuarioDb = struct {
id: u64,
nome: []const u8,
email: []const u8,
hash_senha: []const u8,
};
const UsuarioDto = struct {
id: u64,
nome: []const u8,
email: []const u8,
// Sem hash_senha — dados sensíveis não são expostos
};
test "copiar campos comuns" {
const db = UsuarioDb{
.id = 1,
.nome = "João",
.email = "[email protected]",
.hash_senha = "abc123",
};
const dto = copiarCamposComuns(UsuarioDto, UsuarioDb, db);
try std.testing.expect(dto.id == 1);
try std.testing.expectEqualStrings("João", dto.nome);
try std.testing.expectEqualStrings("[email protected]", dto.email);
}
Exemplo 3: Interface baseada em duck typing
const std = @import("std");
fn tamanho(valor: anytype) usize {
const T = @TypeOf(valor);
// Verificar diferentes formas de obter o "tamanho"
if (@hasField(T, "len")) {
return @field(valor, "len");
} else if (@hasField(T, "count")) {
return @field(valor, "count");
} else if (@hasField(T, "tamanho")) {
return @field(valor, "tamanho");
} else {
@compileError("Tipo " ++ @typeName(T) ++ " não possui campo de tamanho");
}
}
test "duck typing para tamanho" {
const ComLen = struct { len: usize, dados: u32 };
const ComCount = struct { count: usize, tipo: u8 };
const a = ComLen{ .len = 10, .dados = 0 };
const b = ComCount{ .count = 5, .tipo = 1 };
try std.testing.expect(tamanho(a) == 10);
try std.testing.expect(tamanho(b) == 5);
}
Casos de uso comuns
- Mapeamento entre tipos: Copiar campos comuns entre structs de tipos diferentes (ex: DTO para modelo de banco).
- Duck typing em comptime: Verificar se um tipo implementa uma “interface” baseada na presença de campos.
- Código genérico adaptativo: Escrever funções que se adaptam a diferentes tipos sem exigir que todos tenham os mesmos campos.
- Deserialização tolerante: Ignorar campos do JSON/dados que não existem na struct de destino.
- Validação de schemas: Verificar que tipos de configuração possuem os campos obrigatórios.
Validação de contratos em comptime
Um padrão poderoso é usar @hasField junto com @compileError para impor contratos em tipos genéricos, gerando mensagens de erro claras quando um tipo não satisfaz os requisitos:
fn assertTemCamposObrigatorios(comptime T: type) void {
const campos_obrigatorios = [_][]const u8{ "id", "nome", "criado_em" };
inline for (campos_obrigatorios) |campo| {
if (!@hasField(T, campo)) {
@compileError("Tipo " ++ @typeName(T) ++ " deve ter o campo '" ++ campo ++ "'");
}
}
}
fn salvarNoDb(comptime T: type, valor: T) void {
comptime assertTemCamposObrigatorios(T);
// Pode usar valor.id, valor.nome, valor.criado_em com segurança
_ = valor;
}
Essa técnica é especialmente útil em frameworks e bibliotecas onde você precisa garantir que tipos definidos pelo usuário sigam um contrato.
Iteração segura com @hasField e @field
O @hasField é frequentemente usado junto com @field para acessar campos de forma segura em funções genéricas:
fn obterTimestamp(valor: anytype) ?i64 {
const T = @TypeOf(valor);
if (@hasField(T, "timestamp")) {
return @field(valor, "timestamp");
} else if (@hasField(T, "criado_em")) {
return @field(valor, "criado_em");
} else if (@hasField(T, "data")) {
return @field(valor, "data");
}
return null;
}
Esse padrão de “tentar vários nomes de campo” é uma forma de duck typing seguro em Zig.
@hasField vs @hasDecl
É fácil confundir os dois builtins. Use esta tabela como referência rápida:
@hasField | @hasDecl | |
|---|---|---|
| Verifica | Campos de dados da instância | Declarações no namespace do tipo |
| Exemplos | nome, idade, len | init, deinit, CONSTANTE |
| Uso típico | Duck typing de dados, mapeamento de structs | Contratos de métodos/hooks |
Em struct { x: u32, pub fn f() void {} } | "x" → true; "f" → false | "f" → true; "x" → false |
const Exemplo = struct {
campo_de_dados: u32, // campo → @hasField
pub const CONSTANTE = 42; // declaração → @hasDecl
pub fn metodo(self: @This()) void {
_ = self;
}
};
comptime {
_ = @hasField(Exemplo, "campo_de_dados"); // true
_ = @hasField(Exemplo, "CONSTANTE"); // false
_ = @hasDecl(Exemplo, "CONSTANTE"); // true
_ = @hasDecl(Exemplo, "metodo"); // true
_ = @hasDecl(Exemplo, "campo_de_dados"); // false
}
Comparação com reflexão em outras linguagens
Em Python ou JavaScript, a verificação de campos é feita em runtime com hasattr() ou in:
if hasattr(obj, 'nome'): # runtime, com overhead
print(obj.nome)
Em Zig, @hasField opera exclusivamente em tempo de compilação — sem overhead em runtime, com verificação de tipos completa e dead code elimination automática pelo compilador.
Perguntas Frequentes
P: O que é @hasField em Zig?
@hasField(T, "nome") é um builtin comptime que retorna true se o tipo T (struct, union ou enum) possui um campo com aquele nome, e false caso contrário. Não tem custo em runtime e é a base de código genérico e duck typing seguro em Zig.
P: @hasField funciona com unions e enums além de structs?
Sim. @hasField funciona com structs, unions (tagged e bare) e enums. Para unions, verifica os campos (variantes). Para enums, verifica as variantes.
P: Qual a diferença entre @hasField e @hasDecl?
@hasField verifica campos de dados (membros que existem em cada instância da struct). @hasDecl verifica declarações do namespace do tipo — funções, constantes e tipos aninhados. São complementares: use @hasField para campos e @hasDecl para métodos e constantes.
P: É possível obter o tipo de um campo verificado com @hasField?
Sim. Após verificar a existência com @hasField, use @typeInfo(T).@"struct".fields para encontrar o campo e acessar seu tipo, ou use @TypeOf(@field(instancia, "nome")) diretamente.
P: hasField tem custo em runtime?
Não. @hasField é avaliado só em tempo de compilação. Ramos if (@hasField(...)) são resolvidos em comptime e o compilador elimina o código morto do ramo não escolhido.
Builtins relacionados
Veja a referência completa de todos os builtins do Zig: