Design de software

Padrão Iterator em Swift: princípios do Sequence

Ao criar apps com Swift, escrevemos dezenas de vezes por dia loops como for item in array. Como isso é tão natural, só costumamos perguntar o que acontece por dentro depois de tentar percorrer um tipo personalizado com for-in e encontrar um erro de compilação. “O que exatamente acontece dentro desse loop?”

4 min de leitura
Imagem de capa de Padrão Iterator em Swift: princípios do Sequence

Ao criar apps com Swift, escrevemos dezenas de vezes por dia loops como for item in array. Como isso é tão natural, só costumamos perguntar o que acontece por dentro depois de tentar percorrer um tipo personalizado com for-in e encontrar um erro de compilação. “O que exatamente acontece dentro desse loop?”

Em resumo, o loop for-in do Swift é resultado da combinação dos protocolos Sequence e IteratorProtocol. Sequence declara “posso ser percorrido”, enquanto IteratorProtocol realmente extrai os valores: “vou entregar o próximo”.

Neste artigo, vamos ver como os protocolos dividem suas funções, como for-in é expandido internamente e como criar uma sequência personalizada. Se o padrão Iterator parece difícil, leia até o fim.

Qual é a diferença entre Sequence e IteratorProtocol?

Vamos começar pelo ponto mais confuso. Ambos têm relação com iteração, mas suas funções são claramente diferentes.

IteratorProtocol tem apenas um requisito: mutating func next() -> Element?. A cada chamada, retorna um elemento; quando não há mais nada, retorna nil.

Ou seja, o iterator é um “cursor que carrega estado”. Ele lembra até onde leu e avança uma posição sempre que next() é chamado.

Sequence está em um nível mais alto. Sua função é criar e entregar um iterator pelo método makeIterator(). É como dizer: “se quiser me percorrer, use este iterator”.

Resumo:

  • IteratorProtocol: cursor que extrai valores um a um. O essencial é next().
  • Sequence: fábrica que cria esse cursor. O essencial é makeIterator().

Array, dicionário, Set, strings, Range… Todos os tipos iteráveis da biblioteca padrão do Swift seguem Sequence.


Como for-in é expandido internamente?

Este é o ponto central do artigo. O for-in que usamos sem pensar é, na verdade, açúcar sintático (syntactic sugar).

Este é o loop que usamos normalmente.

for number in [1, 2, 3] {
    print(number)
}

O compilador transforma esse código aproximadamente na forma abaixo: cria um iterator com makeIterator() e executa um loop while até next() retornar nil.

var iterator = [1, 2, 3].makeIterator()
while let number = iterator.next() {
    print(number)  // 1, 2, 3 imprimir em ordem
}

for-in não é mágica; no fim, é apenas a combinação de makeIterator() e next().

No fim, eram apenas estas duas chamadas
No fim, eram apenas estas duas chamadas

Conhecendo essa estrutura, fica natural entender por que um tipo que adota apenas Sequence pode ser usado diretamente em for-in: ele só precisa de makeIterator().


Como criar uma sequência personalizada?

Implementar uma ajuda a entender. Vamos usar como exemplo uma sequência que gera infinitamente a sequência de Fibonacci.

O jeito mais simples é adotar Sequence e IteratorProtocol no mesmo tipo pequeno. Assim, a implementação padrão de makeIterator() copia e retorna o próprio tipo.

struct Fibonacci: Sequence, IteratorProtocol {
    var current = 0
    var nextValue = 1
    mutating func next() -> Int? {
        defer { (current, nextValue) = (nextValue, current + nextValue) }
        return current
    }
}

O trabalho de next() é simples: usar defer para calcular antecipadamente o próximo estado antes de retornar o valor atual.

Agora o tipo pode ser usado diretamente em for-in. Como a sequência é infinita, você precisa limitar o número de iterações manualmente.

for n in Fibonacci().prefix(8) {
    print(n)  // 0 1 1 2 3 5 8 13
}
Criei um tipo Fibonacci e usei no for-in
Criei um tipo Fibonacci e usei no for-in

Também é importante poder usar métodos como prefix, map e filter gratuitamente. Ao adotar Sequence, essas operações padrão vêm pelas extensões do protocolo; não é preciso implementá-las.


Perguntas frequentes

P. Basta adotar Sequence ou IteratorProtocol também é obrigatório? R. Para executar for-in, você precisa de um iterator. Porém, como no exemplo acima, se um tipo adota os dois protocolos, não precisa escrever makeIterator() separadamente. Para separar o estado de iteração da coleção, extraia o iterator para um struct próprio.

P. O que acontece se eu percorrer novamente com for-in uma sequência já percorrida? R. Tipos por valor, como Array, criam um novo iterator a cada vez e podem ser percorridos várias vezes com segurança. Já sequências consumíveis, como um stream de rede, só podem ser percorridas uma vez.

P. Qual é a diferença em relação a Collection? R. Collection é um conceito superior que herda de Sequence. Ele exige mais recursos, como acesso repetido por índice, contagem com count e garantia de ordem. Se você só precisa percorrer valores em uma direção, Sequence é suficiente.

Ao desenhar a relação entre Sequence, Iterator e Collection, tudo ficou claro de relance
Ao desenhar a relação entre Sequence, Iterator e Collection, tudo ficou claro de relance

Por trás de uma linha de for-in existe uma divisão clara entre Sequence e IteratorProtocol: o iterator é o cursor, e a sequência é a fábrica. Quando você distingue os dois, criar tipos iteráveis personalizados fica muito mais fácil.

Na próxima vez que usar for-in, pense: “agora next() está sendo chamado”. Recomendo começar criando sua própria sequência de Fibonacci.

Continue lendo