O Modelo de Iterador em Rust
Todas as coleções na biblioteca padrão de Rust - Vec, HashMap, arrays, slices - expõem seus elementos através da mesma pequena interface: um tipo que sabe como produzir "o próximo item" até que acabe. Essa interface, o trait Iterator, é o fio que une loops for, cadeias map/filter e a maior parte do código com sabor funcional que você escreverá em Rust. Entendê-lo como um modelo, em vez de um conjunto de métodos para memorizar, faz com que o restante das páginas desta seção - o próprio trait, adaptadores, consumo e IntoIterator - se encaixem como variações de uma única ideia.
Esta página é a âncora conceitual para a seção. Ela não percorre as APIs de coleção como Noções Básicas de Coleções faz; em vez disso, explica por que os iteradores são preguiçosos, como a propriedade decide qual de iter, iter_mut ou into_iter você deve usar, e o que o compilador realmente está fazendo quando transforma uma cadeia de adaptadores em código de máquina.
Resumo
- Um iterador é qualquer tipo que implementa
Iterator, o que se resume a um método,next, que retornaSome(item)ouNone; todo o resto no ecossistema é construído sobre essa única operação. - Por que Importa: Rust precisa de um vocabulário compartilhado para "uma sequência de valores pela qual posso percorrer", e iteradores permitem que coleções, geradores de valores computados e leitores de I/O falem a mesma língua sem alocar buffers extras ou sacrificar desempenho.
- Conceitos Chave: preguiça, adaptadores vs consumidores, modos de propriedade (
iter,iter_mut,into_iter), abstração de custo zero,IntoIterator. - Quando Usar: Transformar ou filtrar uma sequência sem índices escritos manualmente; escrever código genérico sobre "qualquer coisa iterável"; construir tipos de sequência personalizados; escolher se um loop deve emprestar, mutar ou consumir sua origem.
- Limitações / Trade-offs: Cadeias são de passagem única e se consomem; alguns padrões (olhar para trás com janelas, acesso a índice e vizinho) são genuinamente mais desajeitados com adaptadores do que com um loop indexado simples.
- Tópicos Relacionados: closures e traits
Fn, propriedade e empréstimo, genéricos e limites de trait, slices.
Fundamentos
Em sua essência, Iterator é um trait com um método obrigatório:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Todo o resto - map, filter, take, sum, collect - são métodos padrão construídos sobre chamadas repetidas a next. Um tipo se torna um iterador implementando este único método; os outros aproximadamente setenta métodos do trait vêm de graça.
A propriedade mais importante deste modelo é a preguiça. Escrever (0..10).filter(|n| n % 2 == 0).map(|n| n * 2) não itera sobre dez números. Ele constrói uma pequena struct aninhada - um Filter envolvendo um Map envolvendo um Range - e não faz nenhum trabalho. Nada acontece até que você chame algo que peça repetidamente por next, como um loop for, .collect() ou .sum(). O compilador Rust até anexa #[must_use] aos adaptadores de iterador especificamente para que uma cadeia não utilizada produza um aviso em vez de silenciosamente não fazer nada.
Ajuda separar o vocabulário em duas famílias. Adaptadores (map, filter, zip, enumerate, take, skip, flat_map) envolvem um iterador em outro iterador, transformando o que next eventualmente produzirá. Consumidores (collect, sum, fold, count, for_each, e o consumo implícito dentro de um loop for) são os métodos que realmente chamam next em um loop e produzem um valor final, não iterador. Uma cadeia de adaptadores sem um consumidor no final é inerte - é uma receita, não um resultado.
Mecânicas e Interações
As três maneiras de transformar uma coleção em um iterador - iter(), iter_mut(), into_iter() - não são três métodos não relacionados para memorizar. São a mesma ideia subjacente, "me dê um iterador sobre os elementos desta coleção", expressa através dos três modos de propriedade de Rust.
collection.iter() -> Iterator<Item = &T> (empréstimo compartilhado)
collection.iter_mut() -> Iterator<Item = &mut T> (empréstimo exclusivo)
collection.into_iter() -> Iterator<Item = T> (propriedade, consome a coleção)
iter() percorre a coleção por referência compartilhada, deixando-a utilizável depois. iter_mut() a percorre por referência exclusiva, permitindo que você mutate elementos no local - e, como com qualquer empréstimo &mut, o verificador de empréstimo garante que nenhum outro código possa aliasar a coleção enquanto você o faz. into_iter() move a própria coleção para o iterador, entregando valores de propriedade e deixando o binding original inutilizável. Um simples for x in some_vec descompila para some_vec.into_iter(), que é exatamente por que iterar um Vec por valor dentro de um loop for o move; escrever for x in &some_vec em vez disso chama a implementação IntoIterator em &Vec<T>, que internamente delega para iter().
É o trait IntoIterator fazendo seu trabalho: é o trait que faz os loops for funcionarem, e qualquer tipo que o implementa pode ser inserido em um loop for ou aceito por uma função genérica limitada com impl IntoIterator<Item = T>. O próprio Iterator implementa IntoIterator (retornando a si mesmo), que é por que um loop for aceita alegremente tanto um iterador fresco quanto uma coleção.
A frase "abstração de custo zero", aplicada a cadeias de iteradores, tem um significado específico e verificável: não significa que os adaptadores são gratuitos para raciocinar, apenas que a saída compilada não tem um custo de tempo de execução por usá-los. Cada map/filter/take em uma cadeia é uma struct genérica distinta, monomorfizada por tipo de closure no tempo de compilação. O LLVM então embuti toda a estrutura aninhada e, na maioria dos casos, a funde em um loop indistinguível de um que você teria escrito à mão com índices. Esta é uma propriedade real e mensurável, não linguagem de marketing - mas depende do compilador ser capaz de ver através de toda a cadeia, o que geralmente se mantém para código de linha reta e pode falhar em dispatch dinâmico (Box<dyn Iterator>) ou ramificações imprevisíveis.
// Tipos de adaptadores aninhados são visíveis se você tentar nomeá-los:
fn evens_doubled(v: &[i32]) -> std::iter::Map<std::iter::Filter<std::slice::Iter<i32>, fn(&&i32) -> bool>, fn(&i32) -> i32> {
// Código real usa `impl Iterator<Item = i32>` aqui em vez disso - o tipo concreto
// é impossível de nomear na prática, que é *por que* `impl Trait` existe.
unimplemented!()
}O trecho acima é deliberadamente impraticável: ele mostra que uma cadeia de dois passos já tem um tipo muito desajeitado para escrever, que é precisamente a pressão que levou aos tipos de retorno impl Trait para funções que produzem iteradores.
Considerações Avançadas e Aplicações
Como as cadeias são preguiçosas e de passagem única, algumas decisões estruturais recorrem constantemente assim que o código fica mais complexo.
A ordem dos adaptadores importa tanto para a correção quanto para o custo: filter antes de map pula o trabalho de transformação para elementos descartados, enquanto map antes de filter faz a transformação incondicionalmente. filter_map colapsa um filtro-depois-transformação em uma única passagem, evitando uma alocação intermediária que filter().map() não precisa realmente, mas que um pipeline collect()-depois-iter() ingênuo introduziria.
Consumir um iterador é permanente. Uma vez que next retornou None (ou uma vez que você entregou o iterador para collect), o valor está esgotado; não há "rebobinamento". Código que precisa percorrer os mesmos dados duas vezes deve primeiro collect() em uma coleção de propriedade e iterá-la duas vezes, ou reestruturar para evitar a segunda passagem - não há maneira gratuita de "reproduzir" uma cadeia preguiçosa.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Cadeia de adaptadores de iterador | Lê como a transformação, compila para um loop apertado, compõe | Desajeitado para acesso a vizinhos/olhar para trás, apenas passagem única | Pipelines lineares de transformação/filtragem/agregação |
Loop for indexado manual | Controle total, fácil acesso a vizinhos/janelas | Mais gerenciamento, mais fácil introduzir bugs de off-by-one | Algoritmos que precisam de contexto de posição ou elemento adjacente |
Iteradores paralelos do rayon | Mesmo vocabulário de adaptadores, espalha trabalho intensivo de CPU entre threads | Overhead não vale a pena para sequências pequenas ou limitadas por I/O | Transformações grandes, intensivas em CPU, "embaraçosamente paralelas" |
Em código assíncrono, o modelo de iterador tem um irmão em vez de uma extensão: Stream (do ecossistema futures, e cada vez mais tokio) espelha a preguiça e o vocabulário de adaptadores do Iterator, mas produz itens através de pontos .await em vez de síncronamente. Os dois são conceitualmente da mesma forma - "puxar o próximo item sob demanda" - mas são traits diferentes com consumidores diferentes, então adaptadores de iterador e adaptadores de stream não se misturam sem uma conversão explícita.
Iteradores personalizados também são como Rust modela sequências "infinitas" ou geradas com segurança: uma struct Counter implementando Iterator sem um fim natural é completamente válida, porque a preguiça significa que nada é executado até que algo a limite. Essa também é a armadilha - coletar um iterador ilimitado sem um take(n) primeiro irá travar ou esgotar a memória, pois collect não tem como saber que a sequência é ilimitada até que pare de receber None.
Concepções Errôneas Comuns
- "Iteradores são apenas uma sintaxe mais agradável para loops" - A sintaxe é mais agradável, mas o ponto mais profundo é que uma cadeia é um valor (uma struct inerte) antes de ser uma computação; nada é executado até que um consumidor a acione, o que é fácil de perder vindo de linguagens onde as operações de lista são executadas ansiosamente.
- "
iter()einto_iter()são intercambiáveis" - Eles diferem no que nos entregam (&TvsT) e se a coleção de origem sobrevive depois; escolher o errado é o erro de compilação mais comum relacionado a iteradores para iniciantes. - "Custo zero significa que escrever a cadeia não tem custo" - Significa que o programa compilado não tem custo de tempo de execução extra em comparação com um loop escrito à mão; ler e raciocinar sobre uma cadeia de cinco adaptadores ainda tem um custo cognitivo real, e dispatch dinâmico pesado (
Box<dyn Iterator>) pode reintroduzir sobrecarga de tempo de execução. - "
map/filteralocam uma coleção intermediária a cada passo" - Eles não o fazem; cada adaptador apenas envolve o iterador anterior em uma struct, e nenhum buffer existe até que um consumidor comocollectrealmente aloque um. - "Uma cadeia de iteradores pode ser iterada mais de uma vez" - Uma vez consumido, um iterador está esgotado; reutilizar a mesma cadeia para uma segunda passagem produz silenciosamente um iterador que retorna imediatamente
None, não uma nova execução.
FAQs
Qual é o mínimo que um tipo precisa implementar para se tornar um iterador?
Apenas type Item e fn next(&mut self) -> Option<Self::Item>. Todos os outros métodos (map, filter, sum, collect e dezenas de outros) são métodos padrão derivados de next, então implementar uma função desbloqueia todo o vocabulário de adaptadores.
Por que `let x = v.iter().map(|n| n * 2);` não imprime nada nem faz nenhum trabalho?
Porque a cadeia é preguiçosa - map retorna uma struct Map envolvendo o iterador subjacente, e nenhum código é executado até que algo chame next nele (diretamente, via loop for, ou via um consumidor como collect ou sum). O compilador até avisa sobre valores de iterador não utilizados via #[must_use].
Como decido entre `iter()`, `iter_mut()` e `into_iter()`?
Pergunte o que você precisa fazer com cada elemento: lê-lo e manter a coleção depois (iter()), mutá-lo no local e manter a coleção (iter_mut()), ou tomar posse de cada elemento e desistir da coleção (into_iter()). A escolha espelha emprestar um valor, emprestá-lo mutavelmente ou movê-lo - os mesmos três modos que se aplicam em todos os outros lugares em Rust.
Por que `for x in some_vec { ... }` torna `some_vec` inutilizável depois?
Um for x in collection nu descompila para collection.into_iter(), que consome a coleção por valor. Escrever for x in &some_vec em vez disso chama IntoIterator no tipo de referência, que produz itens emprestados e deixa some_vec intacto.
O que realmente conecta os loops `for` ao trait `Iterator`?
O trait IntoIterator. Um loop for é açúcar sintático que chama .into_iter() em seu lado direito e, em seguida, chama repetidamente .next() no resultado até que ele retorne None. Qualquer tipo que implementa IntoIterator - não apenas o próprio Iterator - pode aparecer após in.
Cinco ou seis adaptadores encadeados realmente custam alguma coisa em tempo de execução?
No caso comum, nenhum custo extra mensurável além de um loop escrito à mão, porque o LLVM embuti e funde as structs de adaptadores aninhadas em código de linha reta. Isso pode falhar quando o compilador perde a visibilidade da cadeia - por exemplo, atrás de objetos de trait Box<dyn Iterator>, que trocam a propriedade de custo zero por segurança de objeto e armazenamento heterogêneo.
Por que `.filter(...).map(...)` às vezes é escrito como `.filter_map(...)` em vez disso?
filter_map funde um filtro-depois-transformação em uma única passagem por um único closure retornando Option<T>, o que é mais direto de ler para esse padrão específico e evita raciocinar sobre duas camadas de adaptadores separadas para o que é conceitualmente uma decisão por elemento.
Posso iterar sobre o mesmo `Vec` duas vezes com o mesmo iterador?
Não - uma vez que um iterador está esgotado (ou movido para um consumidor como collect), ele não pode ser reutilizado. Para percorrer os mesmos dados duas vezes, chame .iter() novamente da coleção original, ou .collect() uma vez em uma coleção de propriedade que você pode iterar repetidamente.
É seguro escrever um iterador que nunca termina?
Sim, e é um padrão normal (um contador infinito, um ciclo repetido) precisamente porque a preguiça significa que um iterador ilimitado não faz nada até ser acionado. O perigo está a jusante: chamar .collect() ou outro consumidor ilimitado nele sem primeiro aplicar .take(n) irá travar ou esgotar a memória, pois nada informa a collect antecipadamente que a sequência nunca para.
Como um iterador se relaciona com um closure passado para `map` ou `filter`?
O closure é armazenado dentro da struct do adaptador (Map, Filter, etc.) e chamado de dentro da implementação next desse adaptador. Esta é uma razão pela qual closures e iteradores são cobertos como um par neste cookbook - a maioria das cadeias de adaptadores não triviais são, estruturalmente, um iterador contendo um closure.
Qual é a diferença entre um adaptador de iterador e um consumidor de iterador?
Adaptadores (map, filter, zip, take) envolvem um iterador em outro iterador e permanecem preguiçosos. Consumidores (collect, sum, fold, for_each e o loop for em si) chamam next em um loop e produzem um resultado concreto, não iterador, que é o que realmente aciona todo o trabalho adiado na cadeia.
Os `Stream`s assíncronos de Rust são apenas iteradores assíncronos?
Conceitualmente sim - Stream espelha a forma preguiçosa, baseada em pull e orientada a adaptadores do Iterator - mas eles são um trait distinto cujos itens chegam através de pontos .await em vez de síncronamente, então adaptadores de Iterator e adaptadores de Stream não são diretamente intercambiáveis sem uma ponte explícita.
Por que o compilador avisa sobre uma cadeia de iteradores que "não faz nada"?
Porque os métodos adaptadores do Iterator são marcados com #[must_use]; se você constrói uma cadeia e nunca a consome (nunca chama um método que aciona next), o compilador assume que isso é um erro, já que uma cadeia preguiçosa sem um consumidor não tem efeito observável algum.
Relacionados
- Noções Básicas de Coleções - o companheiro prático desta página, para escolher entre
Vec,HashMap,BTreeMape similares. - O Trait Iterator - as mecânicas de implementar
Iteratorvocê mesmo, incluindosize_hinteFusedIterator. - Adaptadores de Iterador - um tour prático de
map,filter,zip,flat_mape ordenação de cadeia. - IntoIterator & Propriedade vs. Empréstimo - um olhar mais profundo e baseado em receitas sobre
iter/iter_mut/into_iter. - Padrões de Closure + Iterador em Rust - como closures capturados dentro de adaptadores interagem com a propriedade.
- Iteradores Falíveis & collect() - coletando em
Result<Vec<T>, E>e curto-circuitando em caso de erro.
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024). Ela é conceitual e não está vinculada a uma versão específica de crate além da biblioteca padrão.