Cheatsheet: Iterator em Zig

Iterator em Zig

O padrão Iterator fornece uma maneira de acessar os elementos de uma coleção sequencialmente sem expor sua representação interna. Em Zig, o protocolo de iteração é simples e convencionado: uma struct com um método next() que retorna ?T (optional do tipo do elemento). Quando retorna null, a iteração acabou.

Quando Usar

  • Percorrer coleções customizadas
  • Processamento lazy (sob demanda) de sequências
  • Composição de transformações sem alocação intermediária
  • Gerar sequências infinitas ou muito grandes

Protocolo de Iteração em Zig

const std = @import("std");

// Qualquer struct com next() -> ?T funciona com while
const Contador = struct {
    atual: u32,
    maximo: u32,

    pub fn init(maximo: u32) Contador {
        return .{ .atual = 0, .maximo = maximo };
    }

    pub fn next(self: *Contador) ?u32 {
        if (self.atual >= self.maximo) return null;
        const valor = self.atual;
        self.atual += 1;
        return valor;
    }
};

pub fn main() void {
    var contador = Contador.init(5);

    // Protocolo while com capture
    while (contador.next()) |valor| {
        std.debug.print("{d} ", .{valor}); // 0 1 2 3 4
    }
    std.debug.print("\n", .{});
}

Iteradores da Biblioteca Padrão

const std = @import("std");

pub fn main() void {
    // Split iterator
    var split = std.mem.splitSequence(u8, "Zig,é,incrível", ",");
    while (split.next()) |parte| {
        std.debug.print("'{s}' ", .{parte});
    }
    std.debug.print("\n", .{});

    // Tokenize (ignora delimitadores consecutivos)
    var tokens = std.mem.tokenizeAny(u8, "  Zig  é   incrível  ", " ");
    while (tokens.next()) |token| {
        std.debug.print("'{s}' ", .{token});
    }
    std.debug.print("\n", .{});

    // Window iterator
    const dados = "ABCDEFGH";
    var window = std.mem.window(u8, dados, 3, 1);
    while (window.next()) |w| {
        std.debug.print("{s} ", .{w}); // ABC BCD CDE DEF EFG FGH
    }
    std.debug.print("\n", .{});
}

Iterador Customizado com Estado

const std = @import("std");

const Fibonacci = struct {
    a: u64 = 0,
    b: u64 = 1,
    limite: ?u64 = null,

    pub fn init() Fibonacci {
        return .{};
    }

    pub fn comLimite(limite: u64) Fibonacci {
        return .{ .limite = limite };
    }

    pub fn next(self: *Fibonacci) ?u64 {
        if (self.limite) |lim| {
            if (self.a > lim) return null;
        }
        const valor = self.a;
        const proximo = self.a +| self.b; // saturating add
        self.a = self.b;
        self.b = proximo;
        return valor;
    }
};

pub fn main() void {
    var fib = Fibonacci.comLimite(100);
    while (fib.next()) |n| {
        std.debug.print("{d} ", .{n}); // 0 1 1 2 3 5 8 13 21 34 55 89
    }
    std.debug.print("\n", .{});
}

Composição de Iteradores

const std = @import("std");

fn MapIterator(comptime Src: type, comptime Dst: type) type {
    return struct {
        const Self = @This();
        fonte: *Src,
        transformar: *const fn (std.meta.Elem(Src)) Dst,

        pub fn next(self: *Self) ?Dst {
            const val = self.fonte.next() orelse return null;
            return self.transformar(val);
        }
    };
}

fn FilterIterator(comptime Src: type) type {
    return struct {
        const Self = @This();
        const T = std.meta.Elem(Src);
        fonte: *Src,
        predicado: *const fn (T) bool,

        pub fn next(self: *Self) ?T {
            while (self.fonte.next()) |val| {
                if (self.predicado(val)) return val;
            }
            return null;
        }
    };
}

Iterador sobre Diretório

const std = @import("std");

pub fn main() !void {
    var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
    defer dir.close();

    var iter = dir.iterate();
    while (try iter.next()) |entrada| {
        const tipo: []const u8 = switch (entrada.kind) {
            .file => "ARQ",
            .directory => "DIR",
            else => "???",
        };
        std.debug.print("[{s}] {s}\n", .{ tipo, entrada.name });
    }
}

Considerações de Performance

  • Iteradores são lazy por natureza: o valor é calculado (ou lido) apenas quando next() é chamado. Isso é excelente para sequências grandes — você processa um elemento de cada vez sem alocar um buffer intermediário com todos os elementos.
  • Zero alocação: iteradores customizados em Zig tipicamente vivem na stack. Não há overhead de heap, não há GC pause. Fibonacci, Contador, FilterIterator — todos são structs de poucos bytes na stack.
  • Iteradores compostos com comptime: MapIterator e FilterIterator parametrizados por comptime tipos permitem ao compilador inlinar a função de transformação/predicado. O resultado é equivalente a escrever o loop manualmente.
  • Atenção ao hasNext vs next: o protocolo Zig usa apenas next() -> ?T. Não implemente hasNext separado — isso exige que o iterador “espreite” o próximo valor, o que pode ser caro para iteradores de I/O.

Iterador com Reset e Peek

Para casos onde você precisa olhar o próximo elemento sem avançar:

const PeekableContador = struct {
    atual: u32,
    maximo: u32,
    _peeked: ?u32 = null,

    pub fn peek(self: *PeekableContador) ?u32 {
        if (self._peeked == null) {
            self._peeked = self.next();
        }
        return self._peeked;
    }

    pub fn next(self: *PeekableContador) ?u32 {
        if (self._peeked) |val| {
            self._peeked = null;
            return val;
        }
        if (self.atual >= self.maximo) return null;
        const val = self.atual;
        self.atual += 1;
        return val;
    }
};

O peek é útil em parsers onde você precisa decidir qual caminho seguir com base no próximo token sem consumi-lo.

Erros Comuns

Modificar a coleção durante a iteração: se você remove ou insere elementos em um ArrayList enquanto itera sobre ele com um iterador baseado em índice, o índice fica desincronizado. Prefira coletar os índices a remover e limpar após a iteração.

Iterador com referência à coleção que muda de endereço: se o iterador guarda um ponteiro para um ArrayList e o ArrayList é realocado (por append), o ponteiro fica dangling. Sempre passe o ArrayList como argumento para o iterador depois de todas as inserções, ou use ensureTotalCapacity antes de criar o iterador.

Retornar []T em vez de ?T em next: retornar um slice vazio como sentinela de fim (len == 0) é ambíguo — o iterador pode legitimamente retornar um slice vazio que representa um campo vazio em um CSV, por exemplo. Use sempre ?T onde null significa explicitamente “iteração encerrada”.

Perguntas Frequentes

Como criar um iterador que itera de trás para frente? Implemente um ReverseIterator que guarda o índice atual iniciando em len e decrementa: if (self.idx == 0) return null; self.idx -= 1; return self.dados[self.idx];. O protocolo next() -> ?T funciona igualmente.

Posso usar iteradores Zig com for? O for nativo de Zig (for (slice) |item|) funciona apenas com arrays, slices e ranges. Para iteradores customizados, use while (iter.next()) |val| { ... }. Não existe (ainda) sintaxe especial para iteradores customizados no for.

Como colecionar os resultados de um iterador em um ArrayList? Use um loop com try lista.append(iter.next() orelse break), ou escreva uma função auxiliar: fn coletarIterador(iter: anytype, allocator: Allocator) !ArrayList(...).

Quando Evitar

  • Arrays simples — use for direto: for (array) |item| { ... }
  • Quando todo o conteúdo precisa estar em memória de qualquer forma
  • Operações que precisam de acesso aleatório (índice)

Veja Também

Continue aprendendo Zig

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