Iterator Pattern em Zig — O que é e Como Usar
Definição
O Iterator Pattern (padrão de iteração) em Zig segue a convenção de expor um método next() que retorna ?T — ou seja, um optional que é null quando a iteração termina. Diferente de linguagens com traits/interfaces formais de iterador (como Rust ou Java), Zig usa duck typing via anytype ou convenção explícita. Qualquer struct com um método next() que retorne optional pode ser usada como iterador.
Este padrão é usado extensivamente na biblioteca padrão para tokenização de strings, iteração sobre diretórios, leitura de linhas e muito mais.
Por que Iterator Pattern Importa
- Avaliação preguiçosa: Dados são produzidos sob demanda, sem alocar tudo na memória.
- Composição: Iteradores podem ser encadeados (map, filter, take).
- Convenção simples: Apenas
next() -> ?T— sem interface formal complexa. - Uso com while: Integra naturalmente com
while (iter.next()) |item|.
Exemplo Prático
Iterador Simples
const std = @import("std");
const Intervalo = struct {
atual: usize,
fim: usize,
pub fn init(inicio: usize, fim: usize) Intervalo {
return .{ .atual = inicio, .fim = fim };
}
pub fn next(self: *Intervalo) ?usize {
if (self.atual >= self.fim) return null;
const valor = self.atual;
self.atual += 1;
return valor;
}
};
pub fn main() void {
var iter = Intervalo.init(5, 10);
while (iter.next()) |valor| {
std.debug.print("{} ", .{valor}); // 5 6 7 8 9
}
std.debug.print("\n", .{});
}
Tokenizador de Strings (da std lib)
const std = @import("std");
pub fn main() void {
const csv = "nome,idade,cidade,país";
// std.mem.splitScalar retorna um iterador
var iter = std.mem.splitScalar(u8, csv, ',');
while (iter.next()) |campo| {
std.debug.print("Campo: '{s}'\n", .{campo});
}
// Campo: 'nome'
// Campo: 'idade'
// Campo: 'cidade'
// Campo: 'país'
}
Iterador com Estado Complexo
const std = @import("std");
fn Fibonacci() type {
return struct {
a: u64 = 0,
b: u64 = 1,
contagem: usize = 0,
limite: usize,
const Self = @This();
pub fn init(limite: usize) Self {
return .{ .limite = limite };
}
pub fn next(self: *Self) ?u64 {
if (self.contagem >= self.limite) return null;
self.contagem += 1;
const resultado = self.a;
const novo = self.a +| self.b; // saturating add
self.a = self.b;
self.b = novo;
return resultado;
}
};
}
pub fn main() void {
var fib = Fibonacci().init(10);
while (fib.next()) |valor| {
std.debug.print("{} ", .{valor});
}
// 0 1 1 2 3 5 8 13 21 34
std.debug.print("\n", .{});
}
Convenção do Iterador
// Qualquer struct com este método é um iterador:
pub fn next(self: *Self) ?ItemType {
// Retornar próximo item ou null se acabou
}
// Uso idiomático:
while (iter.next()) |item| {
// processar item
}
Iteradores na std lib
| Iterador | Uso |
|---|---|
std.mem.splitScalar | Dividir string por caractere |
std.mem.splitSequence | Dividir por sequência |
std.mem.tokenizeScalar | Tokenizar ignorando delimitadores consecutivos |
std.mem.window | Janela deslizante sobre slice |
std.fs.Dir.iterate() | Iterar entradas de diretório |
Armadilhas Comuns
- Mutabilidade: O iterador precisa ser
var, nãoconst, poisnext()modifica estado interno. - Consumo único: A maioria dos iteradores só pode ser percorrida uma vez. Para percorrer novamente, crie outro iterador.
- Sem reset: Não há convenção de “reset” — crie uma nova instância.
- null vs fim:
nullsignifica “sem mais itens”, não erro. Para iteradores que podem falhar, o retorno deve ser!?T.
Quando Usar Iterator Pattern
Use o padrão de iterador quando:
- Processamento sequencial: Você precisa percorrer elementos um a um sem carregar tudo na memória.
- Fonte de dados lazy: Os dados são gerados sob demanda (sequências infinitas, leitura de arquivos, streams de rede).
- Composição de transformações: Aplicar filtros e mapeamentos em cadeia antes de consumir os resultados.
- Abstração de origem: A função que consome o iterador não precisa saber se os dados vêm de um array, arquivo ou rede.
Funções Genéricas com Iteradores
Como Zig não tem interfaces formais, use anytype para escrever funções que aceitam qualquer iterador:
const std = @import("std");
// Aceita qualquer tipo que tenha next() -> ?T
fn contar(iter: anytype) usize {
var count: usize = 0;
var it = iter;
while (it.next()) |_| count += 1;
return count;
}
fn primeiro(iter: anytype) ?@TypeOf(iter).Next {
var it = iter;
return it.next();
}
// Coletar todos os itens em um ArrayList
fn coletarEmLista(
iter: anytype,
allocator: std.mem.Allocator,
) !std.ArrayList(@TypeOf(blk: {
var dummy = iter;
break :blk dummy.next().?;
})) {
_ = allocator; // simplificado para exemplo
@compileError("exemplo simplificado");
}
A convenção next() -> ?T funciona como um contrato implícito que o compilador verifica via duck typing.
Boas Práticas
- Declare como
var: O iterador precisa ser mutável, poisnext()altera estado interno.const iter = ...causará erro de compilação ao chamarnext(). - Crie novo iterador para reiterar: Não há convenção de reset. Se precisar percorrer novamente, construa uma nova instância.
- Use
?Tlimpo para iteradores infalíveis: Se a iteração não pode falhar, retorne?T. Reserve!?Tpara iteradores que lêem de fontes que podem dar erro (arquivos, rede). - Documente o comportamento de fim: Deixe claro se
nullsignifica “sem mais dados” ou se há situações onde o iterador para prematuramente.
Termos Relacionados
- Optional — Tipo de retorno
?Tdonext() - Struct — Iteradores são structs com estado
- anytype — Funções genéricas aceitam qualquer iterador
- Slice — Fonte comum de dados para iteração