Exercícios de Rust
Ler sobre Rust é útil. Mas é escrevendo Rust que o conhecimento fixa. Esta página tem 18 problemas pequenos, em ordem do mais fácil para o mais difícil e agrupados por tema.
Em cada um, leia o enunciado, tente resolver sozinho primeiro e só depois abra a solução. É totalmente normal travar ou ver erros do compilador. Isso faz parte do aprendizado, e as mensagens de erro do Rust costumam ser a melhor dica que você vai ter.
Como praticar: abra o Rust Playground em outra aba ou crie um projeto com cargo new practice (veja Começando com Rust). Escreva sua resposta lá. Depois compare com a solução. Se a sua resposta for diferente mas imprimir a mesma coisa, provavelmente está certa. Quase sempre existe mais de um jeito bom de fazer.
O básico
Seção intitulada “O básico”1. FizzBuzz
Seção intitulada “1. FizzBuzz”Imprima os números de 1 a 15. Mas, nos múltiplos de 3, imprima Fizz; nos múltiplos de 5, imprima Buzz; e nos múltiplos dos dois, imprima FizzBuzz.
Dica: use um laço for sobre 1..=15 e o operador % (resto da divisão). Verifique “múltiplo dos dois” primeiro.
Ver solução
fn main() { for n in 1..=15 { if n % 15 == 0 { println!("FizzBuzz"); } else if n % 3 == 0 { println!("Fizz"); } else if n % 5 == 0 { println!("Buzz"); } else { println!("{n}"); } }}Saída:
12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzzA ordem importa. Se você verificar n % 3 == 0 primeiro, o 15 imprime Fizz e nunca chega ao ramo do FizzBuzz. Um número divisível por 3 e por 5 ao mesmo tempo é divisível por 15.
2. Inverter uma string
Seção intitulada “2. Inverter uma string”Escreva uma função reverse(s: &str) -> String que devolva o texto de trás para frente. "roma" deve virar "amor".
Dica: .chars() entrega os caracteres um de cada vez, .rev() inverte um iterador e .collect() consegue montar uma String.
Ver solução
fn reverse(s: &str) -> String { s.chars().rev().collect()}
fn main() { println!("{}", reverse("roma")); println!("{}", reverse("olá")); println!("{}", reverse("pão"));}Saída:
amorálooãpPor que .chars() e não os bytes? Uma String em Rust é UTF-8, então um caractere como ã ocupa dois bytes. Inverter os bytes quebraria o caractere e geraria um texto inválido. .chars() trabalha com caracteres inteiros. Veja Strings em Rust para saber mais.
3. Contar as vogais
Seção intitulada “3. Contar as vogais”Escreva count_vowels(s: &str) -> usize, que conta as vogais (a, e, i, o, u) de uma string. Minúsculas e maiúsculas contam.
Dica: .chars().filter(...).count(). Uma string como "aeiouAEIOU" tem o método .contains(c), que você pode usar para testar cada caractere.
Ver solução
fn count_vowels(s: &str) -> usize { s.chars().filter(|c| "aeiouAEIOU".contains(*c)).count()}
fn main() { println!("{}", count_vowels("Oi, Mundo")); println!("{}", count_vowels("Rust e divertido")); println!("{}", count_vowels("AEIOU e mais nada")); println!("{}", count_vowels("Psst"));}Saída:
46100O filter passa para a closure uma referência a cada item (&char), por isso escrevemos *c para pegar o próprio char. As páginas Closures em Rust e Iteradores em Rust explicam esse padrão.
Atenção: vogais com acento (á, ã, é…) são caracteres diferentes, então esta versão não conta essas vogais. Se quiser contá-las, é só acrescentá-las à string "aeiouAEIOU".
Ownership (posse) e borrowing (empréstimo)
Seção intitulada “Ownership (posse) e borrowing (empréstimo)”4. Corrija o erro de move
Seção intitulada “4. Corrija o erro de move”Este programa não compila. Mude só o parâmetro da função shout e as duas chamadas para que ele imprima OLÁ! duas vezes.
fn shout(text: String) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("olá"); shout(word); shout(word);}error[E0382]: use of moved value: `word` --> src/main.rs:8:11 |6 | let word = String::from("olá"); | ---- move occurs because `word` has type `String`, which does not implement the `Copy` trait7 | shout(word); | ---- value moved here8 | shout(word); | ^^^^ value used here after moveO compilador avisa, em inglês, que word foi usada depois de ser movida (moved): a primeira chamada levou o valor embora, e a segunda tenta usá-lo de novo.
Dica: shout só precisa olhar o texto, não ser dona dele. Veja Ownership (posse).
Ver solução
fn shout(text: &str) { println!("{}!", text.to_uppercase());}
fn main() { let word = String::from("olá"); shout(&word); shout(&word);}Saída:
OLÁ!OLÁ!Quando uma função recebe String, chamá-la move a string para dentro dela, e o main não consegue mais usá-la. Receber &str significa que a função só pega a string emprestada. O compilador também sugere word.clone(), que funciona, mas copia a string inteira sem necessidade. Pegar emprestado é a correção melhor aqui.
5. Corrija o erro de borrowing
Seção intitulada “5. Corrija o erro de borrowing”Este programa também não compila. Faça com que ele imprima Ana, mas continue adicionando Ben à lista.
fn main() { let mut names = vec![String::from("Ana")]; let first = &names[0]; names.push(String::from("Ben")); println!("{first}");}error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable --> src/main.rs:4:5 |3 | let first = &names[0]; | ----- immutable borrow occurs here4 | names.push(String::from("Ben")); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here5 | println!("{first}"); | ----- immutable borrow later used hereEm português: não dá para pegar names emprestado como mutável porque ele já está emprestado como imutável.
Dica: o problema é a ordem. first ainda está em uso quando o push altera o vetor. Veja Borrowing (empréstimo).
Ver solução
fn main() { let mut names = vec![String::from("Ana")]; names.push(String::from("Ben")); let first = &names[0]; println!("{first}");}Saída:
AnaPor que o Rust é tão rígido aqui? O push pode precisar de mais espaço, e aí o vetor move todos os itens para um lugar novo e maior na memória. Nesse caso, first apontaria para o lugar antigo, que não é mais válido. A regra do Rust (vários leitores ou um escritor, nunca os dois ao mesmo tempo) impede esse bug antes mesmo de o programa rodar. Pegar a referência depois do push resolve.
6. Primeira palavra de uma string
Seção intitulada “6. Primeira palavra de uma string”Escreva first_word(s: &str) -> &str, que devolve a primeira palavra (tudo antes do primeiro espaço). Se não houver espaço, devolva a string inteira. Não crie uma String nova: devolva um slice (fatia) da original.
Dica: s.find(' ') devolve um Option<usize> com a posição do primeiro espaço. &s[..i] é um slice do começo até i.
Ver solução
fn first_word(s: &str) -> &str { match s.find(' ') { Some(i) => &s[..i], None => s, }}
fn main() { println!("{:?}", first_word("oi mundo")); println!("{:?}", first_word("Rust")); println!("{:?}", first_word(""));}Saída:
"oi""Rust"""O &str devolvido aponta para a mesma memória da entrada, então nada é copiado. O compilador sabe que o resultado não pode viver mais do que a entrada. É a elisão de lifetimes em ação: com uma referência entrando e uma saindo, você não precisa escrever os lifetimes.
Uma versão mais curta é s.split_whitespace().next().unwrap_or(""), que também pula espaços extras no começo.
Coleções
Seção intitulada “Coleções”7. Maior número de um vetor
Seção intitulada “7. Maior número de um vetor”Escreva largest(numbers: &[i32]) -> Option<i32>. Ela devolve o maior número, ou None se a lista estiver vazia. Desta vez, não use o .max() que já vem pronto: escreva o laço você mesmo.
Dica: comece com o primeiro item como “o maior até agora”. numbers.first() devolve um Option, então ele já cuida do caso da lista vazia para você.
Ver solução
fn largest(numbers: &[i32]) -> Option<i32> { let mut biggest = *numbers.first()?; for &n in numbers { if n > biggest { biggest = n; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[-5, -2, -9])); println!("{:?}", largest(&[]));}Saída:
Some(41)Some(-2)Nonenumbers.first()? devolve None logo de cara se a lista estiver vazia. É por isso que a função devolve Option: “o maior de nada” não tem uma boa resposta, e o Rust obriga você a deixar isso claro. Em código de verdade, numbers.iter().max() faz o mesmo trabalho.
Começar com 0 em vez do primeiro item é um bug clássico: a lista [-5, -2, -9] daria 0, o que está errado.
8. Frequência de palavras
Seção intitulada “8. Frequência de palavras”Conte quantas vezes cada palavra aparece em uma frase. Ignore maiúsculas/minúsculas e pontuação, e imprima as palavras em ordem alfabética.
Dica: use um BTreeMap<String, usize>. Ele funciona como um HashMap, mas mantém as chaves ordenadas. map.entry(word).or_insert(0) entrega um contador para você somar 1.
Ver solução
use std::collections::BTreeMap;
fn main() { let text = "O gato viu o rato. O fim!"; let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for word in text.split_whitespace() { let clean = word .trim_matches(|c: char| !c.is_alphanumeric()) .to_lowercase(); if clean.is_empty() { continue; } *counts.entry(clean).or_insert(0) += 1; }
for (word, count) in &counts { println!("{word}: {count}"); }}Saída:
fim: 1gato: 1o: 3rato: 1viu: 1Por que BTreeMap e não HashMap? Um HashMap entrega os itens em uma ordem imprevisível, que pode mudar de uma execução para outra. Se você precisa da saída ordenada, use um BTreeMap, ou coloque o conteúdo do HashMap em um Vec e ordene.
trim_matches tira o . de rato. e o ! de fim!, e to_lowercase faz O e o virarem a mesma palavra.
Um detalhe: o BTreeMap ordena pelos bytes do texto, então palavras com acento (como água) iriam para o fim da lista, depois do z. Por isso a frase do exemplo não tem acentos.
9. Quadrados dos números pares
Seção intitulada “9. Quadrados dos números pares”A partir de vec![1, 2, 3, 4, 5, 6, 7, 8], crie um vetor novo só com os quadrados dos números pares: [4, 16, 36, 64]. Depois imprima a soma deles. Use métodos de iterador, não um laço for.
Dica: encadeie .iter(), .filter(...), .map(...) e .collect(). Depois, .iter().sum() soma tudo.
Ver solução
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8];
let squares: Vec<i32> = numbers .iter() .filter(|n| *n % 2 == 0) .map(|n| n * n) .collect();
let total: i32 = squares.iter().sum();
println!("{:?}", squares); println!("Soma: {total}");}Saída:
[4, 16, 36, 64]Soma: 120Leia a cadeia de cima para baixo como uma receita: pegue os números, fique com os pares, eleve cada um ao quadrado e coloque tudo em um Vec. Iteradores são preguiçosos (lazy): nada roda de verdade até o .collect() pedir os resultados. Veja Iteradores em Rust.
Tratamento de erros
Seção intitulada “Tratamento de erros”10. Converter e somar números
Seção intitulada “10. Converter e somar números”Escreva sum_numbers(input: &str) -> Result<i32, ParseIntError>. Ela recebe um texto como "1, 2, 3", converte cada pedaço em número e devolve a soma. Se algum pedaço não for um número, devolva o erro em vez de derrubar o programa.
Dica: divida o texto em ',', aplique .trim() em cada pedaço e depois .parse(). Use o operador ? para retornar mais cedo quando houver erro.
Ver solução
use std::num::ParseIntError;
fn sum_numbers(input: &str) -> Result<i32, ParseIntError> { let mut total = 0; for piece in input.split(',') { let n: i32 = piece.trim().parse()?; total += n; } Ok(total)}
fn main() { for input in ["1, 2, 3", "10,20, 30", "4, cinco, 6", ""] { match sum_numbers(input) { Ok(sum) => println!("{input:?} -> a soma é {sum}"), Err(e) => println!("{input:?} -> erro: {e}"), } }}Saída:
"1, 2, 3" -> a soma é 6"10,20, 30" -> a soma é 60"4, cinco, 6" -> erro: invalid digit found in string"" -> erro: cannot parse integer from empty stringAs mensagens de erro saem em inglês porque são escritas pela própria biblioteca padrão do Rust: “invalid digit found in string” quer dizer que apareceu um caractere que não é dígito, e “cannot parse integer from empty string” quer dizer que não dá para converter uma string vazia em número.
O ? faz duas coisas com um único caractere: se parse() der Ok(n), você recebe o n. Se der Err(e), a função devolve Err(e) na hora. É por isso que a função também precisa devolver um Result.
Bônus: Result funciona com .sum(), então esta única linha faz a mesma coisa e para no primeiro erro:
input.split(',').map(|p| p.trim().parse::<i32>()).sum()11. Encadeando Option
Seção intitulada “11. Encadeando Option”Cada usuário pode ter ou não um e-mail. Escreva email_domain(user: &User) -> Option<&str>, que devolve a parte depois do @. Devolva None se não houver e-mail ou se o e-mail não tiver @. Depois, procure usuários pelo nome e imprima o domínio de cada um.
Dica: user.email.as_deref() transforma um Option<String> em um Option<&str>. email.split_once('@') devolve Option<(&str, &str)>. O operador ? também funciona com Option. Veja Option em Rust.
Ver solução
struct User { name: String, email: Option<String>,}
fn email_domain(user: &User) -> Option<&str> { let email = user.email.as_deref()?; let (_, domain) = email.split_once('@')?; Some(domain)}
fn main() { let users = vec![ User { name: String::from("Ben"), email: None }, User { name: String::from("Cleo"), email: Some(String::from("cleo-em-casa")) }, ];
for name in ["Ana", "Ben", "Cleo", "Dan"] { let domain = users .iter() .find(|u| u.name == name) .and_then(email_domain) .unwrap_or("(sem domínio)"); println!("{name}: {domain}"); }}Saída:
Ana: example.comBen: (sem domínio)Cleo: (sem domínio)Dan: (sem domínio)Aqui existem três jeitos diferentes de “falhar”: Dan não existe, Ben não tem e-mail e o e-mail da Cleo não tem @. Cada um é uma etapa com Option, e o ? ou o .and_then() param no primeiro None. Nada de verificações com if, nada de erros de ponteiro nulo. No finalzinho, o .unwrap_or(...) dá um valor padrão.
12. Uma conta bancária
Seção intitulada “12. Uma conta bancária”Crie um struct Account com um titular (owner) e um saldo (balance). Dê a ele os métodos deposit(amount) e withdraw(amount). O withdraw deve devolver Result<u64, String>: o novo saldo se deu certo, ou uma mensagem de erro se não houver dinheiro suficiente. O saldo nunca pode ficar abaixo de zero.
Dica: métodos que alteram a conta recebem &mut self. Verifique o saldo antes de subtrair. Veja Structs em Rust e Result em Rust.
Ver solução
struct Account { owner: String, balance: u64,}
impl Account { fn new(owner: &str) -> Account { Account { owner: owner.to_string(), balance: 0 } }
fn deposit(&mut self, amount: u64) { self.balance += amount; }
fn withdraw(&mut self, amount: u64) -> Result<u64, String> { if amount > self.balance { return Err(format!( "não é possível sacar {amount}: o saldo é só {}", self.balance )); } self.balance -= amount; Ok(self.balance) }}
fn main() { let mut account = Account::new("Ana"); account.deposit(100);
for amount in [30, 50, 40] { match account.withdraw(amount) { Ok(left) => println!("Sacou {amount}, restam {left}"), Err(e) => println!("Erro: {e}"), } }
println!("{} tem {} no final", account.owner, account.balance);}Saída:
Sacou 30, restam 70Sacou 50, restam 20Erro: não é possível sacar 40: o saldo é só 20Ana tem 20 no finalUsar u64 (um número sem sinal) é uma rede de segurança extra: ele simplesmente não consegue guardar um saldo negativo. Se você esquecesse a verificação, self.balance -= amount entraria em panic no modo debug com a mensagem “attempt to subtract with overflow” (tentativa de subtrair com estouro), em vez de ficar negativo sem avisar.
Traits e genéricos
Seção intitulada “Traits e genéricos”13. Um trait Shape
Seção intitulada “13. Um trait Shape”Defina um trait Shape (forma) com dois métodos: area(&self) -> f64 e name(&self) -> String. Implemente esse trait para um Circle (com um raio) e um Rectangle (com largura e altura). Coloque os dois em um mesmo Vec e imprima o nome e a área de cada um com 2 casas decimais.
Dica: para guardar tipos diferentes em um único Vec, use Vec<Box<dyn Shape>>. Veja Traits em Rust e Trait objects (dyn). {:.2} imprime 2 casas decimais.
Ver solução
trait Shape { fn area(&self) -> f64; fn name(&self) -> String;}
struct Circle { radius: f64,}
struct Rectangle { width: f64, height: f64,}
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn name(&self) -> String { format!("Círculo (r = {})", self.radius) }}
impl Shape for Rectangle { fn area(&self) -> f64 { self.width * self.height } fn name(&self) -> String { format!("Retângulo ({} x {})", self.width, self.height) }}
fn main() { let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Rectangle { width: 3.0, height: 4.0 }), Box::new(Circle { radius: 2.5 }), ];
let mut total = 0.0; for shape in &shapes { println!("{}: {:.2}", shape.name(), shape.area()); total += shape.area(); } println!("Área total: {total:.2}");}Saída:
Círculo (r = 1): 3.14Retângulo (3 x 4): 12.00Círculo (r = 2.5): 19.63Área total: 34.78O laço não sabe (nem se importa) qual forma ele tem nas mãos. Ele só sabe que cada item “é um Shape”, então pode chamar area() e name(). Para acrescentar um Triangle depois, basta escrever um impl novo, e o laço não muda nada.
14. Um largest genérico
Seção intitulada “14. Um largest genérico”Volte ao exercício 7, mas faça a função funcionar com qualquer tipo que possa ser comparado: inteiros, decimais, caracteres e strings. A assinatura é largest<T: PartialOrd>(items: &[T]) -> Option<&T>.
Dica: T: PartialOrd quer dizer “T aceita < e >”. Devolva uma referência (&T) para não precisar copiar nem clonar os itens. Veja Genéricos em Rust.
Ver solução
fn largest<T: PartialOrd>(items: &[T]) -> Option<&T> { let mut biggest = items.first()?; for item in items { if item > biggest { biggest = item; } } Some(biggest)}
fn main() { println!("{:?}", largest(&[3, 41, 7, 12])); println!("{:?}", largest(&[2.5, -1.0, 9.75])); println!("{:?}", largest(&['r', 'u', 's', 't'])); println!("{:?}", largest(&["pera", "abacate", "uva"]));
let empty: Vec<i32> = Vec::new(); println!("{:?}", largest(&empty));}Saída:
Some(41)Some(9.75)Some('u')Some("uva")NoneO corpo é quase igual ao do exercício 7. A única mudança de verdade é a parte <T: PartialOrd>. Sem essa restrição (bound), o compilador recusa item > biggest, porque nem todo tipo pode ser comparado. Strings são comparadas em ordem alfabética, e é por isso que "uva" ganha.
Repare no Vec<i32> do vetor vazio: sem nenhum item, o Rust não tem como adivinhar o que é T, então você precisa dizer.
Conjuntos, ordenação, padrões e iteradores
Seção intitulada “Conjuntos, ordenação, padrões e iteradores”15. Encontre os repetidos
Seção intitulada “15. Encontre os repetidos”Uma lista de convidados tem alguns nomes escritos mais de uma vez. Escreva find_repeats(words: &[&str]) -> Vec<String>, que devolve todo nome que aparece mais de uma vez, cada um listado uma única vez, em ordem alfabética. Mostre também quantos convidados diferentes existem.
Dica: um HashSet lembra o que você já viu. O método insert dele devolve false quando o valor já estava lá. Um HashSet não tem uma ordem fixa, então ordene o resultado no final.
Ver solução
use std::collections::HashSet;
fn find_repeats(words: &[&str]) -> Vec<String> { let mut seen = HashSet::new(); let mut repeats = HashSet::new();
for &word in words { // insert devolve false se a palavra já estava no conjunto if !seen.insert(word) { repeats.insert(word); } }
let mut result: Vec<String> = repeats.into_iter().map(String::from).collect(); result.sort(); result}
fn main() { let guests = ["ana", "bia", "caio", "bia", "davi", "ana", "bia"]; println!("Repetidos: {:?}", find_repeats(&guests));
let unique: HashSet<&str> = guests.iter().copied().collect(); println!("Convidados diferentes: {}", unique.len());
println!("Repetidos: {:?}", find_repeats(&["x", "y", "z"]));}Saída:
Repetidos: ["ana", "bia"]Convidados diferentes: 4Repetidos: []Por que dois conjuntos? O seen guarda todos os nomes vistos até agora. O repeats guarda os nomes que encontramos pela segunda vez. "bia" aparece três vezes, mas, como repeats é um conjunto, ela só é guardada uma vez.
Coletar em um HashSet também é o jeito mais rápido de contar valores únicos: os duplicados simplesmente somem. Sem a linha result.sort(), a ordem dos repetidos poderia mudar de uma execução para outra.
16. Ordene palavras pelo tamanho
Seção intitulada “16. Ordene palavras pelo tamanho”Ordene uma lista de palavras da mais curta para a mais longa. Quando duas palavras tiverem o mesmo tamanho, coloque-as em ordem alfabética, ignorando maiúsculas e minúsculas. Depois compare o seu resultado com um .sort() simples.
Dica: sort_by recebe uma closure que compara dois itens e devolve um Ordering. O .then_with(...) acrescenta uma segunda comparação, que só conta em caso de empate. Veja Ordenação e busca.
Ver solução
fn main() { let mut words = vec!["pear", "Fig", "banana", "apple", "kiwi", "Cherry", "date"];
let mut plain = words.clone(); plain.sort(); println!("Ordem simples: {plain:?}");
words.sort_by(|a, b| { a.len() .cmp(&b.len()) .then_with(|| a.to_lowercase().cmp(&b.to_lowercase())) }); println!("Por tamanho: {words:?}");}Saída:
Ordem simples: ["Cherry", "Fig", "apple", "banana", "date", "kiwi", "pear"]Por tamanho: ["Fig", "date", "kiwi", "pear", "apple", "banana", "Cherry"]As palavras ficaram em inglês de propósito: .len() conta bytes, e letras acentuadas como “ç” ou “ã” ocupam 2 bytes cada, o que mudaria o resultado.
Olhe a ordem simples: "Cherry" e "Fig" vêm primeiro. Strings são comparadas pelos códigos dos caracteres, e toda letra maiúscula vem antes de toda minúscula. É por isso que comparamos as versões em to_lowercase() no desempate.
Um jeito mais curto de escrever a mesma regra é words.sort_by_key(|w| (w.len(), w.to_lowercase())). Tuplas são comparadas campo por campo, então o tamanho decide primeiro e a palavra em minúsculas desempata.
17. Custo de frete com match guards
Seção intitulada “17. Custo de frete com match guards”Escreva shipping_cost(p: Package) -> String para este enum, usando um único match:
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}As regras: retirada (Pickup) é grátis. Cartas de até 20 g custam R$ 1, cartas mais pesadas custam R$ 3. Encomendas (Parcel) acima de 30 kg não podem ser enviadas. Encomendas expressas custam R$ 10 mais R$ 2 por kg. As outras encomendas custam R$ 5 mais R$ 1 por kg.
Dica: um braço do match pode ter um if depois do padrão, chamado de guard (guarda): Package::Letter { grams } if grams <= 20 => .... Os braços são verificados de cima para baixo, então coloque a regra de “pesado demais” antes da regra do expresso. Veja Pattern matching.
Ver solução
enum Package { Letter { grams: u32 }, Parcel { kg: u32, express: bool }, Pickup,}
fn shipping_cost(p: Package) -> String { match p { Package::Pickup => String::from("grátis (retirada)"), Package::Letter { grams } if grams <= 20 => String::from("R$ 1 (carta simples)"), Package::Letter { grams } => format!("R$ 3 (carta grande, {grams} g)"), Package::Parcel { kg, .. } if kg > 30 => format!("pesado demais para enviar ({kg} kg)"), Package::Parcel { kg, express: true } => format!("R$ {} (expresso)", 10 + kg * 2), Package::Parcel { kg, express: false } => format!("R$ {} (encomenda normal)", 5 + kg), }}
fn main() { let packages = vec![ Package::Letter { grams: 15 }, Package::Letter { grams: 80 }, Package::Parcel { kg: 4, express: false }, Package::Parcel { kg: 4, express: true }, Package::Parcel { kg: 35, express: true }, Package::Pickup, ];
for p in packages { println!("{}", shipping_cost(p)); }}Saída:
R$ 1 (carta simples)R$ 3 (carta grande, 80 g)R$ 9 (encomenda normal)R$ 18 (expresso)pesado demais para enviar (35 kg)grátis (retirada)A encomenda expressa de 35 kg é a mais interessante. Ela combina com dois braços, mas só o primeiro vale. Se você colocasse o braço do expresso acima do braço de “pesado demais”, ele cobraria alegremente R$ 80 por uma encomenda que nem pode ser enviada.
Repare que o último braço diz express: false em vez de ... Isso não é obrigatório, mas deixa o braço dizer exatamente o que ele trata. O compilador continua verificando se todos os casos foram cobertos. Os guards não entram nessa verificação, e é por isso que o braço Package::Letter { grams } sem guard precisa estar lá.
18. Limpe um relatório de estoque
Seção intitulada “18. Limpe um relatório de estoque”O relatório de um depósito tem um item por linha, como "maçãs 12". Algumas linhas estão quebradas: vazias, ou com uma quantidade que não é número. Usando métodos de iteradores (sem laço for), fique só com as linhas que podem ser convertidas e têm quantidade acima de zero. Mostre-as como nome: quantidade, separadas por vírgulas, e depois mostre o total.
Dica: .lines() divide o texto em linhas. filter_map mantém só os valores Some que a closure devolve, então filtra e converte num passo só. .ok() transforma um Result em um Option. Veja Iteradores em Rust.
Ver solução
fn main() { let report = "maçãs 12\nbananas x\nperas 0\n\ncerejas 40\nameixas 7";
let in_stock: Vec<(&str, u32)> = report .lines() .filter_map(|line| line.split_once(' ')) .filter_map(|(name, count)| count.parse::<u32>().ok().map(|n| (name, n))) .filter(|&(_, n)| n > 0) .collect();
let list: Vec<String> = in_stock .iter() .map(|(name, n)| format!("{name}: {n}")) .collect(); let total: u32 = in_stock.iter().map(|(_, n)| n).sum();
println!("{}", list.join(", ")); println!("Total de itens: {total}");}Saída:
maçãs: 12, cerejas: 40, ameixas: 7Total de itens: 59Acompanhe uma linha de cada vez. A linha vazia não tem espaço, então o split_once devolve None e o primeiro filter_map a descarta. "bananas x" se divide sem problema, mas "x" não vira número, então o segundo filter_map a descarta. "peras 0" passa pelos dois, e aí o filter a remove porque a quantidade é zero.
Fazemos collect em um Vec uma vez e depois percorremos ele duas vezes: uma para montar o texto e outra para o total. Um iterador só pode ser usado uma vez, então guardar os dados já limpos é mais simples do que converter tudo duas vezes.
Para onde ir agora
Seção intitulada “Para onde ir agora”Se você terminou os 18, parabéns. Isso é progresso de verdade. 🦀
- Construa um programa completo: experimente o projeto Jogo de adivinhação, o Contador de palavras ou o de Lista de tarefas.
- Está se preparando para uma vaga? Leia as Perguntas de entrevista sobre Rust.
- Quer praticar ainda mais? Os links abaixo têm muitos problemas extras.
Para saber mais
Seção intitulada “Para saber mais”Todos os links abaixo estão em inglês.
- Rustlings: o conjunto oficial de pequenos programas quebrados que você corrige um por um, no terminal. Um ótimo próximo passo depois desta página.
- Trilha de Rust do Exercism: cerca de 100 problemas com testes, e feedback gratuito de mentores humanos sobre as suas soluções.
- Rust by Example: exemplos curtos para rodar, para cada tema. Ajuda quando você trava na sintaxe de um exercício.
- Trait
Iterator— documentação da std: todos os métodos de iteradores (filter_map,fold,zipe muitos outros), cada um com um exemplo pequeno. HashSet— documentação da std: todos os métodos de conjuntos, incluindounion,intersectionedifference.