Até agora trabalhamos com dados de tamanho fixo — arrays, tuplas, tipos primitivos. Mas programas reais precisam de coleções que crescem e encolhem em tempo de execução: listas de usuários, índices de palavras, conjuntos de permissões. A biblioteca padrão de Rust oferece coleções ricas e eficientes. Neste artigo, vamos explorar as três mais usadas: Vec<T>, HashMap<K, V> e HashSet<T>.
Vec<T> — a lista dinâmica
Vec<T> é provavelmente a coleção mais usada em Rust. É uma lista de elementos do mesmo tipo, armazenada contiguamente na memória, que cresce dinamicamente conforme necessário.
Criando vetores
fn main() {
// Vetor vazio com tipo explícito
let mut numeros: Vec<i32> = Vec::new();
// Vetor criado com a macro vec!
let frutas = vec!["maçã", "banana", "laranja"];
// Adicionando elementos
numeros.push(10);
numeros.push(20);
numeros.push(30);
println!("{:?}", numeros); // [10, 20, 30]
println!("{:?}", frutas); // ["maçã", "banana", "laranja"]
}
A macro vec! é um atalho conveniente para criar vetores com valores iniciais. O tipo é inferido automaticamente a partir dos elementos.
Acessando elementos
fn main() {
let v = vec![10, 20, 30, 40, 50];
// Acesso por índice — panic se fora dos limites
let terceiro = v[2];
println!("Terceiro: {terceiro}");
// Acesso seguro com get — retorna Option
match v.get(10) {
Some(valor) => println!("Valor: {valor}"),
None => println!("Índice fora dos limites"),
}
// Primeiro e último
println!("Primeiro: {:?}", v.first());
println!("Último: {:?}", v.last());
}
A distinção entre v[i] e v.get(i) é importante: v[i] entra em panic se o índice for inválido, enquanto v.get(i) retorna Option<&T> — seguro por natureza. Em código robusto, prefira get quando o índice vem de entrada externa.
Iterando sobre vetores
fn main() {
let mut precos = vec![29.90, 49.90, 15.50, 89.00];
// Iteração por referência imutável
println!("Preços:");
for preco in &precos {
println!(" R$ {preco:.2}");
}
// Iteração por referência mutável
for preco in &mut precos {
*preco *= 0.9; // 10% de desconto
}
println!("\nCom desconto:");
for preco in &precos {
println!(" R$ {preco:.2}");
}
}
Note o *preco na iteração mutável — o asterisco desreferencia o ponteiro para acessar o valor subjacente. Sem ele, estaríamos tentando multiplicar a referência, não o valor.
Métodos essenciais de Vec
fn main() {
let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
println!("Tamanho: {}", v.len());
println!("Vazio? {}", v.is_empty());
v.sort();
println!("Ordenado: {:?}", v);
v.dedup(); // remove duplicatas consecutivas
println!("Sem duplicatas: {:?}", v);
v.retain(|&x| x > 3); // mantém apenas elementos > 3
println!("Apenas > 3: {:?}", v);
let removido = v.pop(); // remove e retorna o último
println!("Removido: {:?}", removido);
println!("Restante: {:?}", v);
// Concatenando vetores
let mut a = vec![1, 2, 3];
let b = vec![4, 5, 6];
a.extend(b);
println!("Concatenado: {:?}", a);
}
Saída:
Tamanho: 10
Vazio? false
Ordenado: [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
Sem duplicatas: [1, 2, 3, 4, 5, 6, 9]
Apenas > 3: [4, 5, 6, 9]
Removido: Some(9)
Restante: [4, 5, 6]
Concatenado: [1, 2, 3, 4, 5, 6]
Vec com enum — coleções heterogêneas
Vec<T> exige que todos os elementos sejam do mesmo tipo. Mas e se você precisar de tipos diferentes? Use um enum:
#[derive(Debug)]
enum Celula {
Inteiro(i64),
Decimal(f64),
Texto(String),
Vazio,
}
fn main() {
let linha: Vec<Celula> = vec![
Celula::Texto(String::from("Ana Silva")),
Celula::Inteiro(30),
Celula::Decimal(1.68),
Celula::Vazio,
];
for celula in &linha {
match celula {
Celula::Inteiro(v) => print!("{v:>10} "),
Celula::Decimal(v) => print!("{v:>10.2} "),
Celula::Texto(v) => print!("{v:>10} "),
Celula::Vazio => print!("{:>10} ", "—"),
}
}
println!();
}
Essa técnica é muito usada para representar linhas de planilhas, células de tabelas, ou qualquer estrutura tabular com tipos mistos.
HashMap<K, V> — o dicionário de Rust
HashMap<K, V> mapeia chaves do tipo K para valores do tipo V. Internamente usa hashing para acesso em tempo médio O(1).
Criando e populando
use std::collections::HashMap;
fn main() {
let mut estoque: HashMap<String, u32> = HashMap::new();
estoque.insert(String::from("maçã"), 150);
estoque.insert(String::from("banana"), 80);
estoque.insert(String::from("laranja"), 200);
println!("{:?}", estoque);
}
Note o use std::collections::HashMap — diferente de Vec e Option, o HashMap não é importado automaticamente e precisa ser trazido ao escopo.
Acessando valores
use std::collections::HashMap;
fn main() {
let mut capitais = HashMap::new();
capitais.insert("Brasil", "Brasília");
capitais.insert("Argentina", "Buenos Aires");
capitais.insert("Chile", "Santiago");
// get retorna Option<&V>
match capitais.get("Brasil") {
Some(capital) => println!("Capital do Brasil: {capital}"),
None => println!("País não encontrado"),
}
// Acesso direto — panic se não existir
let capital = capitais["Argentina"];
println!("Capital da Argentina: {capital}");
// Verificando existência
if capitais.contains_key("Chile") {
println!("Chile está no mapa");
}
}
Iterando sobre HashMap
use std::collections::HashMap;
fn main() {
let mut notas = HashMap::new();
notas.insert("Ana", 9.5);
notas.insert("Carlos", 7.2);
notas.insert("Maria", 8.8);
// Iteração — ordem não garantida
for (aluno, nota) in ¬as {
println!("{aluno}: {nota:.1}");
}
// Coletando e ordenando para exibição consistente
let mut ranking: Vec<(&str, f64)> = notas
.iter()
.map(|(&k, &v)| (k, v))
.collect();
ranking.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
println!("\n── Ranking ──");
for (i, (aluno, nota)) in ranking.iter().enumerate() {
println!("{}. {aluno}: {nota:.1}", i + 1);
}
}
entry — inserção condicional
Um padrão extremamente comum é: inserir um valor se a chave não existir, ou atualizar se existir. A API entry resolve isso elegantemente:
use std::collections::HashMap;
fn main() {
let texto = "olá mundo olá rust mundo rust rust";
let mut frequencia: HashMap<&str, u32> = HashMap::new();
for palavra in texto.split_whitespace() {
// entry retorna a entrada existente ou cria uma nova
let contador = frequencia.entry(palavra).or_insert(0);
*contador += 1;
}
let mut pares: Vec<(&&str, &u32)> = frequencia.iter().collect();
pares.sort_by(|a, b| b.1.cmp(a.1));
for (palavra, count) in pares {
println!("{palavra}: {count}x");
}
}
Saída:
rust: 3x
olá: 2x
mundo: 2x
entry(chave).or_insert(valor_padrão) é o padrão idiomático para contagem de frequência — um dos mais usados em código Rust real.
HashSet<T> — conjuntos sem duplicatas
HashSet<T> é uma coleção de valores únicos — sem chaves, sem ordem garantida, sem duplicatas. É como um HashMap onde só importa a presença ou ausência de um elemento.
Criando e usando
use std::collections::HashSet;
fn main() {
let mut permissoes: HashSet<String> = HashSet::new();
permissoes.insert(String::from("ler"));
permissoes.insert(String::from("escrever"));
permissoes.insert(String::from("executar"));
permissoes.insert(String::from("ler")); // duplicata ignorada
println!("Total de permissões: {}", permissoes.len()); // 3
if permissoes.contains("escrever") {
println!("Pode escrever.");
}
permissoes.remove("executar");
println!("{:?}", permissoes);
}
Operações de conjunto
O verdadeiro poder de HashSet está nas operações matemáticas de conjuntos:
use std::collections::HashSet;
fn main() {
let time_a: HashSet<&str> = ["Ana", "Carlos", "Maria", "João"]
.iter().cloned().collect();
let time_b: HashSet<&str> = ["Maria", "João", "Pedro", "Lucia"]
.iter().cloned().collect();
// Interseção — jogadores em ambos os times
let em_ambos: HashSet<&&str> = time_a.intersection(&time_b).collect();
println!("Em ambos: {:?}", em_ambos);
// União — todos os jogadores
let todos: HashSet<&&str> = time_a.union(&time_b).collect();
println!("Total único: {}", todos.len());
// Diferença — só no time A
let apenas_a: HashSet<&&str> = time_a.difference(&time_b).collect();
println!("Só no time A: {:?}", apenas_a);
// Diferença simétrica — em um ou outro, mas não nos dois
let exclusivos: HashSet<&&str> = time_a
.symmetric_difference(&time_b)
.collect();
println!("Exclusivos: {:?}", exclusivos);
}
Um programa completo: análise de texto
Vamos combinar as três coleções num programa que analisa um texto e gera estatísticas:
use std::collections::{HashMap, HashSet};
fn analisar_texto(texto: &str) -> () {
let palavras: Vec<&str> = texto
.split_whitespace()
.map(|p| p.trim_matches(|c: char| !c.is_alphabetic()))
.filter(|p| !p.is_empty())
.collect();
let total = palavras.len();
// Frequência com HashMap
let mut frequencia: HashMap<&str, usize> = HashMap::new();
for palavra in &palavras {
*frequencia.entry(palavra).or_insert(0) += 1;
}
// Palavras únicas com HashSet
let unicas: HashSet<&&str> = frequencia.keys().collect();
// Top 5 mais frequentes
let mut ranking: Vec<(&&str, &usize)> = frequencia.iter().collect();
ranking.sort_by(|a, b| b.1.cmp(a.1));
println!("── Análise de Texto ─────────────");
println!("Total de palavras : {total}");
println!("Palavras únicas : {}", unicas.len());
println!("Riqueza vocabular : {:.1}%",
unicas.len() as f64 / total as f64 * 100.0);
println!("\nTop 5 mais frequentes:");
for (palavra, count) in ranking.iter().take(5) {
let barra = "█".repeat(**count);
println!(" {:12} {:3}x {}", palavra, count, barra);
}
}
fn main() {
let texto = "Rust é uma linguagem de programação de sistemas \
que roda incrivelmente rápido previne falhas de \
segmentação e garante segurança de threads Rust \
é diferente de todas as outras linguagens Rust \
oferece controle de baixo nível com ergonomia \
de alto nível a comunidade Rust é acolhedora";
analisar_texto(texto);
}
Saída:
── Análise de Texto ─────────────
Total de palavras : 43
Palavras únicas : 34
Riqueza vocabular : 79.1%
Top 5 mais frequentes:
Rust 3x ███
de 5x █████
é 3x ███
nível 2x ██
linguagem 1x █
Ownership e coleções
As regras de ownership se aplicam integralmente às coleções. Quando você insere um valor numa coleção, ela toma posse dele:
use std::collections::HashMap;
fn main() {
let chave = String::from("nome");
let valor = String::from("Ana");
let mut mapa = HashMap::new();
mapa.insert(chave, valor);
// println!("{chave}"); // ERRO: chave foi movida para o mapa
// println!("{valor}"); // ERRO: valor foi movido para o mapa
}
Se quiser manter o acesso às strings originais, use referências — mas então o mapa só pode ser usado enquanto as referências forem válidas, e o compilador garantirá isso.
Escolhendo a coleção certa
A escolha entre coleções deve ser guiada pela necessidade:
Use Vec quando a ordem importa, quando você acessa elementos por índice, quando precisa iterar em sequência, ou quando simplesmente precisa de uma lista.
Use HashMap quando precisa associar chaves a valores e fazer buscas rápidas por chave — inventários, configurações, índices, caches.
Use HashSet quando precisa de unicidade, quando precisa verificar pertencimento rapidamente, ou quando quer fazer operações de conjunto como interseção e união.
A biblioteca padrão oferece ainda BTreeMap, BTreeSet, VecDeque, LinkedList e outras — cada uma com suas características de desempenho e ordenação. Mas Vec, HashMap e HashSet cobrem a vasta maioria dos casos de uso cotidianos.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 8 — Common Collections — https://doc.rust-lang.org/book/ch08-00-common-collections.html
- Rust by Example — Vec — https://doc.rust-lang.org/rust-by-example/std/vec.html
- Rust by Example — HashMap — https://doc.rust-lang.org/rust-by-example/std/hash.html
- Rust Standard Library Docs — std::collections — referência completa de todas as coleções — https://doc.rust-lang.org/std/collections/index.html
- "Rust Collections" — Jon Gjengset — análise de desempenho e uso idiomático — https://www.youtube.com/c/JonGjengset
- Rustlings, seção
collections— https://github.com/rust-lang/rustlings