Iteradores do PHP

Artigo que explica sobre a interface Iterator do PHP, um exemplo e sua vantagem.

Introdução

No artigo As interfaces Iterator, ArrayAccess e Countable do PHP comentei rapidamente sobre iteradores, porém não mostrei um exemplo real e as vantagens desse tipo de recurso. Nesse artigo, vou me aprofundar um pouco mais nesses pontos.

Conceito

Conforme mencionado no artigo anterior, Iterator é uma interface especial do PHP que permite dar um comportamento aos objetos das classes que implementam tal interface: o comportamento de serem percorríveis por um foreach. Portanto, implementando os métodos exigidos pela interface, é possível especificar quais elementos e índices serão devolvidos a cada iteração, além de ser possível especificar se a iteração acabou e o que fazer quando for necessário reiniciar o iterador para a posição inicial.

Exemplo de Iterador

Neste exemplo, vou mostrar como implementar uma classe que vai simular uma sequência de números que começam em uma posição inicial e termina numa posição final.

<?php
class Lista implements Iterator {
    private $inicio;
    private $fim;
    private $atual;
    private $indice;

    public function __construct($inicio, $fim) { 
        if ($fim < $inicio) { 
            throw new \InvalidArgumentException('Fim precisa ser maior ou igual ao inicio');
        }
        $this->inicio = $inicio;
        $this->fim = $fim;
        $this->atual = $inicio;
        $this->indice = 0;
    }

    public function current() {
        return $this->atual;
    }

    public function key() {
        return $this->indice;
    }

    public function next() {
        if ($this->valid()) {
            $this->indice += 1;
            $this->atual += 1;
        }
    }

    public function rewind() {
        $this->indice = 0;
        $this->atual = $this->inicio;
    }

    public function valid(): bool {
        return $this->atual <= $this->fim;
    }
}

Agora vamos ver um exemplo de como utilizar a classe para criar um objeto que poderá ser percorrido como se fosse um array:

$obj = new Lista(10, 20);
foreach ($obj as $key => $val) {
    printf("%d => %d\n", $key, $val);
}

O código acima irá imprimir os valores de 10 até 20, com o índice na esquerda (iniciados na posição 0).

Observe na classe de exemplo que temos apenas 4 atributos. Portanto, poderíamos especificar valores bem grandes para o $fim, que a classe continuaria armazenando apenas os 4 atributos.

Vantagens dos Iteradores

A principal vantagem dos iteradores é que eles podem economizar muito mais memória do que se tivéssemos usando os arrays convencionais.

O exemplo que mostramos acima poderia ser feito com o seguinte código usando array:

$arr = range(10, 20);
foreach ($arr as $key => $val) {
    printf("%d => %d\n", $key, $val);
}

Apesar de ser muito mais simples se usar a função range e percorrer o array, observe que o array criado terá 10 posições. Isso significa que se criássemos um array com uma posição de fim muito alta, seriam alocadas posições de memória para armazenar cada um dos valores.

Fazendo um teste rápido com um range de 10 até 1000000 (dez até um milhão), a versão com iterador consome 2MB, enquanto a versão com array consome 34MB.

Por outro lado, se quiséssemos modificar alguma posição do array, conseguiríamos facilmente. Já com a versão usando classe, precisaríamos implementar a interface ArrayAccess para "simular" a atribuição de um valor específico para determinada posição e, consequentemente, quando essa posição fosse percorrida no iterador, ela deveria identificar que o valor a ser retornado é o valor que foi atribuído manualmente. E, caso não haja esse valor atribuído, poderia devolver o valor determinístico para aquela posição. A implementação disso com classe seria bem mais complexa para ser implementada do que a classe apresentada anteriormente.

Portanto, use iteradores quando a economia de memória for um requisito importante a ser considerado e o problema possibilite uma abordagem com iteradores. Lembre-se também que, dependendo do caso, é possível criar Iteradores a partir de funções Generator, que são relativamente mais simples de se implementar, mas também possuem algumas limitações. Veja o exemplo anterior usando função generator:

function gen($inicio, $fim) {
    for ($i = 0, $val = $inicio; $val <= $fim; $val++, $i++) {
        yield $i => $val;
    }
}

$obj = gen(10, 20);
foreach ($arr as $key => $val) {
    printf("%d => %d\n", $key, $val);
}

Como é de se imaginar, os iteradores devolvidos pelas funções generator também consomem pouca memória, pois não alocam o valor para cada posição percorrida.

A interface Traversable

Vale mencionar que o PHP também oferece uma interface chamada Traversable. Essa interface define que um objeto será percorrível por um foreach. A diferença para a interface Iterator é que a Traversable é uma interface reservada para uso apenas pelos módulos do PHP (escritos em C++), e não devem ser usada pelas classes escritas em PHP. Desta forma, os módulos do PHP conseguem gerar classes que produzem objetos que podem ser percorríveis, mesmo sem implementar explicitamente todos os métodos previstos na interface Iterator. Na verdade, a própria interface Iterator é uma extensão da interface Traversable, o que faz com que os objetos de classes que implementem essa interface sejam reconhecidos por ambas as interfaces.

O pseudo-tipo iterable

Ainda relacionado com este assunto, existe um pseudo-tipo chamado iterable, que foi adicionado desde o PHP 7.1. Este pseudo-tipo pode ser usado como type hint de parâmetros ou retornos de funções para indicar que o valor pode ser tanto um array (que não é um objeto em PHP) quanto um objeto que possua o contrato com Traversable. Ou seja, se a sua função apenas percorre um parâmetro usando um foreach e não utiliza o operador [] pra acessar nenhum valor desse parâmetro, nem para definir um novo valor, então você pode usar esse pseudo-tipo para deixar sua função mais flexível, já que passará a aceitar tanto arrays quanto objejtos Traversable. De modo análogo, se você especifica um retorno do tipo iterable, então está dizendo ao usuário de sua função que esse retorno não poderá utilizar o operador [] sobre o resultado, apenas poderá percorrê-lo com um foreach.

1 comentário