Pular para o conteúdo

Iteradores em Rust

Um iterador é qualquer coisa que consegue produzir uma sequência de valores, um de cada vez. Você já usa iteradores desde a página do For, mesmo sem chamá-los por esse nome: todo laço for sobre uma coleção funciona com um iterador por baixo dos panos.

fn main() {
let numbers = vec![1, 2, 3];
let mut iter = numbers.iter();
println!("{:?}", iter.next()); // Some(1)
println!("{:?}", iter.next()); // Some(2)
println!("{:?}", iter.next()); // Some(3)
println!("{:?}", iter.next()); // None
}

Saída:

Some(1)
Some(2)
Some(3)
None
▶ Experimente você mesmo

.next() retorna um Option: Some(valor) para cada item e None quando os itens acabam. No fundo, um laço for só fica chamando .next() várias vezes até receber None.

.map() recebe uma closure (função anônima) e a aplica a cada item, gerando um novo iterador:

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
}

Saída:

[2, 4, 6, 8, 10]
▶ Experimente você mesmo

É o .collect() que transforma o iterador de volta em uma coleção concreta (aqui, um Vec<i32>). Sem ele, você teria só um iterador parado ali, sem nada calculado ainda.

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let evens: Vec<&i32> = numbers.iter().filter(|n| **n % 2 == 0).collect();
println!("{:?}", evens);
}

Saída:

[2, 4, 6]
▶ Experimente você mesmo

O **n duplo parece estranho. Acontece que o .filter() entrega para a closure uma referência para uma referência (&&i32), então é preciso desreferenciar duas vezes para comparar com um número comum. É um detalhe bem comum; não se preocupe com a explicação completa por enquanto.

É aqui que encadear métodos de iterador começa a valer a pena: você junta vários passos em um único fluxo fácil de ler:

fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6];
let result: Vec<i32> = numbers
.iter()
.filter(|n| **n % 2 == 0) // fica só com os números pares
.map(|n| n * 10) // multiplica cada um por 10
.collect();
println!("{:?}", result);
}

Saída:

[20, 40, 60]
▶ Experimente você mesmo

Compare com escrever a mesma lógica usando um laço for manual e um Vec vazio onde você vai dando push. Não está errado, mas a versão encadeada se lê quase como “filtre, depois multiplique, depois junte”, que é exatamente a sequência de passos que você está descrevendo.

fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let total: i32 = numbers.iter().sum();
let count = numbers.iter().count();
let has_even = numbers.iter().any(|n| n % 2 == 0);
println!("soma: {total}, quantidade: {count}, tem par: {has_even}");
}

Saída:

soma: 15, quantidade: 5, tem par: true
▶ Experimente você mesmo

O .sum() soma tudo, o .count() diz quantos itens existem e o .any() responde à pergunta “pelo menos um item atende à condição?”. Esses são só alguns dos métodos que todo iterador tem. Existem muitos outros, como enumerate, zip, take e rev. Na página de Iteradores personalizados, você vai criar o seu próprio iterador e ver que ele ganha todos esses métodos de graça.

Pegue os números de 1 a 10, fique só com os ímpares, eleve cada um ao quadrado e some todos. Faça tudo em uma única cadeia de iteradores, sem nenhuma variável mut. Dica: encadeie .filter(), .map() e .sum(), e dê um tipo ao resultado, como let total: i32.

Ver solução
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total: i32 = numbers
.iter()
.filter(|n| **n % 2 == 1) // fica com 1, 3, 5, 7, 9
.map(|n| n * n) // 1, 9, 25, 49, 81
.sum();
println!("soma dos quadrados ímpares: {total}");
}

Saída:

soma dos quadrados ímpares: 165
▶ Experimente você mesmo

O Rust precisa do : i32 em total porque o .sum() consegue somar em vários tipos numéricos diferentes, e ele não tem como adivinhar qual você quer.

Comece com vec!["the", "quick", "brown", "fox", "jumps"]. Monte um novo Vec<String> só com as palavras de mais de 3 letras, em maiúsculas, e imprima-o com {:?}. Dica: .filter(|w| w.len() > 3) fica com as palavras longas, e .to_uppercase() transforma um &str em uma String em maiúsculas.

(As palavras ficam em inglês de propósito: o .len() conta bytes, e letras acentuadas como “ã” ou “é” ocupam 2 bytes, o que mudaria o resultado do filtro.)

Ver solução
fn main() {
let words = vec!["the", "quick", "brown", "fox", "jumps"];
let loud: Vec<String> = words
.iter()
.filter(|w| w.len() > 3)
.map(|w| w.to_uppercase())
.collect();
println!("{:?}", loud);
}

Saída:

["QUICK", "BROWN", "JUMPS"]
▶ Experimente você mesmo

O words continua igual. O .iter() só o pega emprestado, então você ainda pode usar words depois da cadeia.

  • Um iterador produz valores um de cada vez com .next(), que retorna um Option.
  • Laços for usam iteradores por baixo dos panos.
  • .map() transforma cada item; .filter() fica só com os que atendem a uma condição; .collect() transforma o resultado de volta em uma coleção concreta.
  • Encadear métodos de iterador costuma ser mais claro do que um laço manual com um acumulador mutável.

Teste o que aprendeu

1. O que .next() retorna em um iterador?

2. O que .collect() faz no fim de uma cadeia como .iter().map(...).collect()?

3. Em .filter(|n| **n % 2 == 0), por que n é desreferenciado duas vezes?

4. O que faz um laço for funcionar quando ele percorre uma coleção?

5. O que o .filter() mantém no iterador resultante?

Pontuação: 0 / 5