Rust Cheatsheet - Referência Rápida
Guia de consulta rápida com os principais conceitos da linguagem Rust. Use Ctrl+F para buscar o que precisa.
Variáveis e Tipos
Declaração de Variáveis
let x = 5; // imutável (padrão)
let mut y = 10; // mutável
let z: i32 = 42; // com anotação de tipo
const MAX: u32 = 100; // constante (sempre tipada, UPPER_CASE)
static NOME: &str = "Rust"; // variável estática (tempo de vida 'static)
Tipos Primitivos
| Tipo | Descrição | Exemplo |
|---|
i8, i16, i32, i64, i128 | Inteiro com sinal | let x: i32 = -42; |
u8, u16, u32, u64, u128 | Inteiro sem sinal | let x: u32 = 42; |
isize, usize | Tamanho do ponteiro | let i: usize = 0; |
f32, f64 | Ponto flutuante | let pi: f64 = 3.14; |
bool | Booleano | let ativo = true; |
char | Caractere Unicode | let c = 'á'; |
() | Unit (vazio) | let vazio = (); |
[T; N] | Array de tamanho fixo | let arr = [1, 2, 3]; |
(T, U, ..) | Tupla | let t = (1, "oi", true); |
String vs &str
// &str - string slice, referência imutável, tamanho fixo
let s1: &str = "olá mundo"; // literal, alocada no binário
let s2: &str = &outra_string[0..3]; // slice de uma String
// String - tipo heap, mutável, tamanho dinâmico
let s3: String = String::from("olá");
let s4: String = "olá".to_string();
let s5: String = format!("{} {}", "olá", "mundo");
// Conversões
let para_string: String = s1.to_string(); // &str -> String
let para_str: &str = &s3; // String -> &str (deref coercion)
let para_str2: &str = s3.as_str(); // String -> &str (explícito)
Shadowing e Conversão de Tipos
let x = 5;
let x = x + 1; // shadowing: novo binding com mesmo nome
let x = "agora é texto"; // pode mudar o tipo com shadowing
// Casting
let inteiro = 42i32;
let flutuante = inteiro as f64;
let byte = 255u8;
// Parsing
let numero: i32 = "42".parse().unwrap();
let numero2 = "42".parse::<i32>().unwrap(); // turbofish
Funções e Closures
Funções
// Função simples
fn somar(a: i32, b: i32) -> i32 {
a + b // sem ponto-e-vírgula = retorno implícito
}
// Sem retorno (retorna ())
fn imprimir(msg: &str) {
println!("{}", msg);
}
// Retorno antecipado
fn dividir(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
return None; // retorno explícito com 'return'
}
Some(a / b)
}
// Múltiplos retornos via tupla
fn min_max(lista: &[i32]) -> (i32, i32) {
let min = *lista.iter().min().unwrap();
let max = *lista.iter().max().unwrap();
(min, max)
}
// Parâmetros genéricos
fn maior<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Closures
// Closure sem parâmetros
let saudar = || println!("Olá!");
// Closure com parâmetros
let somar = |a: i32, b: i32| a + b;
// Closure com corpo em bloco
let processar = |x: i32| {
let resultado = x * 2;
resultado + 1
};
// Closure capturando variável do escopo
let fator = 3;
let multiplicar = |x| x * fator; // captura 'fator' por referência
let mover = move |x| x * fator; // captura 'fator' por valor (move)
// Closure como parâmetro de função
fn aplicar<F: Fn(i32) -> i32>(f: F, valor: i32) -> i32 {
f(valor)
}
let resultado = aplicar(|x| x * 2, 5); // 10
Traits de Closure
| Trait | Descrição | Captura |
|---|
Fn | Pode ser chamada múltiplas vezes | &self (referência imutável) |
FnMut | Pode ser chamada múltiplas vezes, pode mutar estado | &mut self (referência mutável) |
FnOnce | Pode ser chamada uma única vez | self (toma ownership) |
fn executar_fn(f: impl Fn()) { f(); f(); }
fn executar_fn_mut(mut f: impl FnMut()) { f(); f(); }
fn executar_fn_once(f: impl FnOnce()) { f(); }
// Higher-order: função que retorna closure
fn criar_somador(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
let soma5 = criar_somador(5);
println!("{}", soma5(10)); // 15
Controle de Fluxo
if / else
let x = 5;
if x > 10 {
println!("maior que 10");
} else if x > 0 {
println!("positivo");
} else {
println!("zero ou negativo");
}
// if como expressão
let descricao = if x > 0 { "positivo" } else { "não positivo" };
// if let - desestruturação condicional
let valor: Option<i32> = Some(42);
if let Some(v) = valor {
println!("Valor: {}", v);
}
match
let x = 3;
match x {
1 => println!("um"),
2 | 3 => println!("dois ou três"), // múltiplos padrões
4..=10 => println!("entre 4 e 10"), // range inclusivo
n if n < 0 => println!("negativo"), // guard
_ => println!("outro"), // wildcard (padrão)
}
// match como expressão
let texto = match x {
1 => "um",
2 => "dois",
_ => "outro",
};
// match com desestruturação
let ponto = (3, -5);
match ponto {
(0, 0) => println!("origem"),
(x, 0) => println!("no eixo x em {}", x),
(0, y) => println!("no eixo y em {}", y),
(x, y) => println!("ponto ({}, {})", x, y),
}
// match com enum
enum Comando {
Sair,
Mover { x: i32, y: i32 },
Escrever(String),
Cor(u8, u8, u8),
}
let cmd = Comando::Mover { x: 10, y: 20 };
match cmd {
Comando::Sair => println!("saindo"),
Comando::Mover { x, y } => println!("mover para ({}, {})", x, y),
Comando::Escrever(texto) => println!("{}", texto),
Comando::Cor(r, g, b) => println!("cor: {}, {}, {}", r, g, b),
}
Loops
// loop infinito
let mut contador = 0;
let resultado = loop {
contador += 1;
if contador == 10 {
break contador * 2; // loop pode retornar valor com break
}
};
// while
let mut n = 5;
while n > 0 {
println!("{}!", n);
n -= 1;
}
// while let
let mut pilha = vec![1, 2, 3];
while let Some(topo) = pilha.pop() {
println!("{}", topo);
}
// for - iteração sobre range
for i in 0..5 {
println!("{}", i); // 0, 1, 2, 3, 4
}
for i in 0..=5 {
println!("{}", i); // 0, 1, 2, 3, 4, 5
}
// for - iteração sobre coleção
let nomes = vec!["Ana", "Bia", "Carlos"];
for nome in &nomes { // referência (não consome)
println!("{}", nome);
}
for (i, nome) in nomes.iter().enumerate() {
println!("{}: {}", i, nome); // com índice
}
// labels para loops aninhados
'externo: for i in 0..5 {
for j in 0..5 {
if i + j > 4 {
break 'externo; // sai do loop externo
}
}
}
Ownership e Borrowing
Regras de Ownership
- Cada valor tem exatamente um dono (owner).
- Quando o dono sai de escopo, o valor é descartado (drop).
- Pode haver apenas uma das seguintes situações por vez:
- Uma referência mutável (
&mut T) - Qualquer número de referências imutáveis (
&T)
// Move (tipos que não implementam Copy)
let s1 = String::from("olá");
let s2 = s1; // s1 foi MOVIDO para s2
// println!("{}", s1); // ERRO: s1 não é mais válido
// Clone (cópia explícita)
let s3 = String::from("olá");
let s4 = s3.clone(); // cópia profunda
println!("{} {}", s3, s4); // ambos válidos
// Copy (tipos primitivos são copiados, não movidos)
let x = 5;
let y = x; // cópia (i32 implementa Copy)
println!("{} {}", x, y); // ambos válidos
Borrowing (Empréstimo)
fn calcular_tamanho(s: &String) -> usize {
s.len()
// s é devolvido automaticamente (é apenas empréstimo)
}
let s = String::from("olá");
let tam = calcular_tamanho(&s); // empréstimo imutável
println!("{} tem {} bytes", s, tam); // s ainda é válido
// Referência mutável
fn adicionar_texto(s: &mut String) {
s.push_str(" mundo");
}
let mut s = String::from("olá");
adicionar_texto(&mut s);
println!("{}", s); // "olá mundo"
// Regra: não pode ter &mut junto com & ao mesmo tempo
let mut dados = String::from("abc");
let r1 = &dados; // ok: referência imutável
let r2 = &dados; // ok: outra referência imutável
// let r3 = &mut dados; // ERRO: não pode emprestar como mutável
println!("{} {}", r1, r2);
// r1 e r2 não são mais usados depois daqui (NLL)
let r3 = &mut dados; // ok agora: r1 e r2 já saíram de escopo (NLL)
r3.push_str("d");
Lifetimes (Tempos de Vida)
// Lifetime explícito - garante que a referência retornada vive o suficiente
fn mais_longo<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Lifetime em struct
struct Trecho<'a> {
texto: &'a str,
}
impl<'a> Trecho<'a> {
fn novo(texto: &'a str) -> Self {
Trecho { texto }
}
}
// Lifetime estático - vive durante toda a execução do programa
let s: &'static str = "Eu vivo para sempre";
// Regras de elisão (o compilador infere automaticamente):
// 1. Cada parâmetro de referência recebe seu próprio lifetime
// 2. Se há exatamente um lifetime de entrada, ele é usado na saída
// 3. Se um dos parâmetros é &self ou &mut self, seu lifetime é usado na saída
Structs e Enums
Structs
// Struct clássica
struct Usuario {
nome: String,
email: String,
idade: u32,
ativo: bool,
}
// Instanciação
let usuario = Usuario {
nome: String::from("Ana"),
email: String::from("ana@email.com"),
idade: 30,
ativo: true,
};
// Field init shorthand (quando variável tem mesmo nome do campo)
let nome = String::from("Bia");
let email = String::from("bia@email.com");
let usuario2 = Usuario { nome, email, idade: 25, ativo: true };
// Struct update syntax
let usuario3 = Usuario {
nome: String::from("Carlos"),
..usuario2 // pega os demais campos de usuario2
};
// Tuple struct
struct Cor(u8, u8, u8);
struct Ponto(f64, f64, f64);
let vermelho = Cor(255, 0, 0);
let origem = Ponto(0.0, 0.0, 0.0);
// Unit struct (sem campos)
struct Marcador;
Implementação (impl)
struct Retangulo {
largura: f64,
altura: f64,
}
impl Retangulo {
// Função associada (construtor - sem self)
fn novo(largura: f64, altura: f64) -> Self {
Retangulo { largura, altura }
}
fn quadrado(lado: f64) -> Self {
Retangulo { largura: lado, altura: lado }
}
// Método (recebe &self)
fn area(&self) -> f64 {
self.largura * self.altura
}
fn perimetro(&self) -> f64 {
2.0 * (self.largura + self.altura)
}
// Método que modifica o struct (&mut self)
fn redimensionar(&mut self, fator: f64) {
self.largura *= fator;
self.altura *= fator;
}
// Método que consome o struct (self)
fn em_tupla(self) -> (f64, f64) {
(self.largura, self.altura)
}
}
let mut r = Retangulo::novo(10.0, 5.0);
println!("Área: {}", r.area()); // 50.0
r.redimensionar(2.0);
let tupla = r.em_tupla(); // r é consumido
Enums
// Enum simples
enum Direcao {
Norte,
Sul,
Leste,
Oeste,
}
// Enum com dados
enum Mensagem {
Sair, // sem dados
Mover { x: i32, y: i32 }, // campos nomeados
Escrever(String), // String
Cor(u8, u8, u8), // tupla
}
impl Mensagem {
fn processar(&self) {
match self {
Mensagem::Sair => println!("Saindo..."),
Mensagem::Mover { x, y } => println!("Mover para ({}, {})", x, y),
Mensagem::Escrever(texto) => println!("{}", texto),
Mensagem::Cor(r, g, b) => println!("Cor: ({}, {}, {})", r, g, b),
}
}
}
// Enum com derive
#[derive(Debug, Clone, PartialEq)]
enum Status {
Pendente,
EmProgresso(f64), // porcentagem
Concluido,
Erro(String),
}
Traits
Definição e Implementação
// Definindo um trait
trait Resumo {
// Método obrigatório
fn resumir(&self) -> String;
// Método com implementação padrão
fn previa(&self) -> String {
format!("{}...", &self.resumir()[..20])
}
}
// Implementando para um struct
struct Artigo {
titulo: String,
autor: String,
conteudo: String,
}
impl Resumo for Artigo {
fn resumir(&self) -> String {
format!("{}, por {} - {}", self.titulo, self.autor, &self.conteudo[..50])
}
// previa() usa a implementação padrão
}
// Implementando para outro tipo
struct Tweet {
usuario: String,
texto: String,
}
impl Resumo for Tweet {
fn resumir(&self) -> String {
format!("@{}: {}", self.usuario, self.texto)
}
}
Trait Bounds (Restrições de Tipo)
// Usando impl Trait (syntax sugar)
fn notificar(item: &impl Resumo) {
println!("Novidade: {}", item.resumir());
}
// Usando trait bound (equivalente)
fn notificar2<T: Resumo>(item: &T) {
println!("Novidade: {}", item.resumir());
}
// Múltiplos bounds
fn exibir(item: &(impl Resumo + std::fmt::Display)) {
println!("{}", item);
}
// Com where (mais legível para muitos bounds)
fn processar<T, U>(t: &T, u: &U) -> String
where
T: Resumo + Clone,
U: Resumo + std::fmt::Debug,
{
format!("{} - {:?}", t.resumir(), u)
}
// Retornando impl Trait
fn criar_resumivel() -> impl Resumo {
Tweet {
usuario: String::from("bot"),
texto: String::from("Olá mundo!"),
}
}
Trait Objects (Despacho Dinâmico)
// Trait object com Box<dyn Trait>
fn obter_resumivel(tipo: &str) -> Box<dyn Resumo> {
match tipo {
"artigo" => Box::new(Artigo {
titulo: "Titulo".into(),
autor: "Autor".into(),
conteudo: "Conteúdo longo aqui que ultrapassa cinquenta caracteres facilmente".into(),
}),
_ => Box::new(Tweet {
usuario: "user".into(),
texto: "tweet".into(),
}),
}
}
// Vetor de trait objects
let itens: Vec<Box<dyn Resumo>> = vec![
Box::new(Artigo { /* ... */ }),
Box::new(Tweet { /* ... */ }),
];
for item in &itens {
println!("{}", item.resumir());
}
Traits Comuns da Biblioteca Padrão
| Trait | Uso | Derivável |
|---|
Debug | Formatação com {:?} | Sim |
Display | Formatação com {} | Nao |
Clone | Cópia explícita (.clone()) | Sim |
Copy | Cópia implícita (stack only) | Sim |
PartialEq / Eq | Comparação == e != | Sim |
PartialOrd / Ord | Comparação <, >, etc. | Sim |
Hash | Hashing para HashMap/HashSet | Sim |
Default | Valor padrão (Default::default()) | Sim |
From / Into | Conversão entre tipos | Nao |
Iterator | Iteração com next() | Nao |
Drop | Limpeza ao sair de escopo | Nao |
Deref / DerefMut | Dereferência customizada | Nao |
Send | Pode ser enviado entre threads | Auto |
Sync | Pode ser compartilhado entre threads | Auto |
// Derive automático
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Config {
nome: String,
valor: i32,
}
// Implementação manual de Display
use std::fmt;
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}={}", self.nome, self.valor)
}
}
// From/Into
impl From<(String, i32)> for Config {
fn from((nome, valor): (String, i32)) -> Self {
Config { nome, valor }
}
}
let c: Config = ("chave".to_string(), 42).into();
Collections
Vec (Vetor Dinâmico)
// Criação
let mut v1: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3, 4, 5];
let v3 = vec![0; 10]; // 10 elementos, todos zero
let v4: Vec<i32> = (0..10).collect();
// Adicionar/remover
v1.push(1);
v1.push(2);
v1.push(3);
let ultimo = v1.pop(); // Some(3)
v1.insert(0, 99); // inserir na posição 0
v1.remove(0); // remover da posição 0
// Acessar
let primeiro = &v2[0]; // panic se fora dos limites
let segundo = v2.get(1); // retorna Option<&T>
// Iteração
for val in &v2 {
println!("{}", val);
}
for val in &mut v1 {
*val *= 2;
}
// Métodos úteis
v1.len(); // tamanho
v1.is_empty(); // está vazio?
v1.contains(&2); // contém o valor?
v1.sort(); // ordenar
v1.dedup(); // remover duplicatas consecutivas
v1.retain(|&x| x > 0); // manter apenas positivos
v1.extend(&[10, 20, 30]); // adicionar vários
let fatia: &[i32] = &v1[1..3]; // slice
HashMap
use std::collections::HashMap;
// Criação
let mut mapa: HashMap<String, i32> = HashMap::new();
let mapa2: HashMap<&str, i32> = HashMap::from([
("um", 1),
("dois", 2),
("três", 3),
]);
// Inserir
mapa.insert("pontos".to_string(), 100);
// Acessar
let valor = mapa.get("pontos"); // Option<&i32>
let valor2 = mapa["pontos"]; // panic se não existe
// Entry API (inserir se não existe)
mapa.entry("pontos".to_string()).or_insert(0);
*mapa.entry("pontos".to_string()).or_insert(0) += 10;
// Contagem de palavras com Entry API
let texto = "olá mundo olá rust olá";
let mut contagem: HashMap<&str, i32> = HashMap::new();
for palavra in texto.split_whitespace() {
*contagem.entry(palavra).or_insert(0) += 1;
}
// Iteração
for (chave, valor) in &mapa {
println!("{}: {}", chave, valor);
}
// Métodos úteis
mapa.len();
mapa.is_empty();
mapa.contains_key("pontos");
mapa.remove("pontos");
mapa.keys(); // iterador sobre chaves
mapa.values(); // iterador sobre valores
HashSet
use std::collections::HashSet;
let mut conjunto: HashSet<i32> = HashSet::new();
conjunto.insert(1);
conjunto.insert(2);
conjunto.insert(3);
conjunto.insert(2); // duplicata, não é inserida
let a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [2, 3, 4].into_iter().collect();
// Operações de conjunto
let uniao: HashSet<_> = a.union(&b).collect(); // {1,2,3,4}
let intersecao: HashSet<_> = a.intersection(&b).collect(); // {2,3}
let diferenca: HashSet<_> = a.difference(&b).collect(); // {1}
let simetrica: HashSet<_> = a.symmetric_difference(&b).collect(); // {1,4}
conjunto.contains(&2); // true
conjunto.remove(&2);
conjunto.len();
BTreeMap e VecDeque
use std::collections::BTreeMap;
use std::collections::VecDeque;
// BTreeMap - como HashMap, mas com chaves ordenadas
let mut bt: BTreeMap<&str, i32> = BTreeMap::new();
bt.insert("banana", 3);
bt.insert("abacaxi", 1);
bt.insert("cereja", 2);
// Iteração é sempre em ordem alfabética das chaves
for (chave, valor) in &bt {
println!("{}: {}", chave, valor); // abacaxi, banana, cereja
}
bt.range("abacaxi"..="banana"); // range query
// VecDeque - fila de duas pontas (deque)
let mut deque: VecDeque<i32> = VecDeque::new();
deque.push_back(1); // adicionar no final
deque.push_back(2);
deque.push_front(0); // adicionar no início
deque.pop_front(); // remover do início: Some(0)
deque.pop_back(); // remover do final: Some(2)
Error Handling
Option
// Option<T> = Some(T) | None
fn encontrar(lista: &[i32], alvo: i32) -> Option<usize> {
for (i, &val) in lista.iter().enumerate() {
if val == alvo {
return Some(i);
}
}
None
}
let resultado = encontrar(&[10, 20, 30], 20);
// Tratando Option
match resultado {
Some(indice) => println!("Encontrado no índice {}", indice),
None => println!("Não encontrado"),
}
// Métodos úteis de Option
let x: Option<i32> = Some(42);
x.unwrap(); // 42 (panic se None)
x.unwrap_or(0); // 42 (ou 0 se None)
x.unwrap_or_default(); // 42 (ou Default::default() se None)
x.unwrap_or_else(|| calcular_padrao()); // com closure
x.expect("mensagem"); // 42 (panic com mensagem se None)
x.is_some(); // true
x.is_none(); // false
x.map(|v| v * 2); // Some(84)
x.and_then(|v| if v > 0 { Some(v) } else { None }); // flatmap
x.filter(|&v| v > 0); // Some(42)
x.or(Some(0)); // Some(42), usa alternativa se None
x.zip(Some("a")); // Some((42, "a"))
// if let e while let
if let Some(valor) = x {
println!("{}", valor);
}
Result
// Result<T, E> = Ok(T) | Err(E)
use std::fs;
use std::io;
use std::num::ParseIntError;
fn ler_numero(caminho: &str) -> Result<i32, String> {
let conteudo = fs::read_to_string(caminho)
.map_err(|e| format!("Erro ao ler arquivo: {}", e))?;
let numero: i32 = conteudo.trim().parse()
.map_err(|e| format!("Erro ao converter: {}", e))?;
Ok(numero)
}
// Tratando Result
match ler_numero("dados.txt") {
Ok(n) => println!("Número: {}", n),
Err(e) => eprintln!("Erro: {}", e),
}
// Métodos úteis de Result
let r: Result<i32, String> = Ok(42);
r.unwrap(); // 42 (panic se Err)
r.unwrap_or(0); // 42 (ou 0 se Err)
r.expect("falhou"); // 42 (panic com mensagem se Err)
r.is_ok(); // true
r.is_err(); // false
r.ok(); // Option<T>: Some(42)
r.err(); // Option<E>: None
r.map(|v| v * 2); // Ok(84)
r.map_err(|e| format!("Erro: {}", e));
r.and_then(|v| Ok(v + 1)); // flatmap
r.or(Ok(0)); // Ok(42), usa alternativa se Err
Operador ? (Propagação de Erros)
use std::io;
use std::fs;
// O operador ? propaga o erro automaticamente
fn ler_config() -> Result<String, io::Error> {
let conteudo = fs::read_to_string("config.toml")?; // propaga Err
Ok(conteudo)
}
// Encadeando com ?
fn obter_porta() -> Result<u16, Box<dyn std::error::Error>> {
let config = fs::read_to_string("config.toml")?;
let porta: u16 = config.trim().parse()?;
Ok(porta)
}
// Erro customizado
#[derive(Debug)]
enum AppErro {
Io(io::Error),
Parse(std::num::ParseIntError),
Custom(String),
}
impl std::fmt::Display for AppErro {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppErro::Io(e) => write!(f, "Erro de IO: {}", e),
AppErro::Parse(e) => write!(f, "Erro de parse: {}", e),
AppErro::Custom(msg) => write!(f, "{}", msg),
}
}
}
impl std::error::Error for AppErro {}
impl From<io::Error> for AppErro {
fn from(e: io::Error) -> Self { AppErro::Io(e) }
}
impl From<std::num::ParseIntError> for AppErro {
fn from(e: std::num::ParseIntError) -> Self { AppErro::Parse(e) }
}
Iterators
Criando e Consumindo Iteradores
let v = vec![1, 2, 3, 4, 5];
// Três formas de criar iteradores de coleções
v.iter(); // itera sobre &T
v.iter_mut(); // itera sobre &mut T
v.into_iter(); // itera sobre T (consome a coleção)
// Ranges como iteradores
(0..10); // 0 a 9
(0..=10); // 0 a 10
('a'..='z'); // 'a' a 'z'
Adaptadores (Lazy - não executam até serem consumidos)
let v = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// map - transforma cada elemento
let dobros: Vec<i32> = v.iter().map(|x| x * 2).collect();
// filter - mantém apenas os que satisfazem o predicado
let pares: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
// filter_map - filtra e transforma em uma operação
let resultados: Vec<i32> = vec!["1", "abc", "3"]
.iter()
.filter_map(|s| s.parse::<i32>().ok())
.collect(); // [1, 3]
// flat_map - map que achata o resultado
let palavras: Vec<&str> = vec!["olá mundo", "boa noite"]
.iter()
.flat_map(|s| s.split_whitespace())
.collect(); // ["olá", "mundo", "boa", "noite"]
// enumerate - adiciona índice
for (i, val) in v.iter().enumerate() {
println!("{}: {}", i, val);
}
// zip - combina dois iteradores
let nomes = vec!["Ana", "Bia"];
let idades = vec![30, 25];
let pares: Vec<_> = nomes.iter().zip(idades.iter()).collect();
// [("Ana", 30), ("Bia", 25)]
// chain - concatena dois iteradores
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let todos: Vec<_> = a.iter().chain(b.iter()).collect();
// [1, 2, 3, 4, 5, 6]
// take / skip
let primeiros: Vec<_> = v.iter().take(3).collect(); // [1, 2, 3]
let restante: Vec<_> = v.iter().skip(7).collect(); // [8, 9, 10]
// take_while / skip_while
let ate: Vec<_> = v.iter().take_while(|&&x| x < 5).collect();
let depois: Vec<_> = v.iter().skip_while(|&&x| x < 5).collect();
// peekable - permite espiar o próximo sem consumir
let mut iter = v.iter().peekable();
if iter.peek() == Some(&&1) {
println!("Começa com 1");
}
// chunks (em slices)
for pedaco in v.chunks(3) {
println!("{:?}", pedaco); // [1,2,3], [4,5,6], [7,8,9], [10]
}
// windows (em slices)
for janela in v.windows(3) {
println!("{:?}", janela); // [1,2,3], [2,3,4], [3,4,5], ...
}
// step_by
let de_dois: Vec<_> = (0..10).step_by(2).collect(); // [0, 2, 4, 6, 8]
Consumidores
let v = vec![1, 2, 3, 4, 5];
// collect - transforma iterador em coleção
let conjunto: HashSet<_> = v.iter().collect();
let mapa: HashMap<_, _> = vec![("a", 1), ("b", 2)].into_iter().collect();
// fold - acumula um resultado
let soma = v.iter().fold(0, |acc, &x| acc + x); // 15
let produto = v.iter().fold(1, |acc, &x| acc * x); // 120
// reduce - como fold, mas usa o primeiro elemento como acumulador
let soma = v.iter().copied().reduce(|a, b| a + b); // Some(15)
// sum e product
let soma: i32 = v.iter().sum(); // 15
let produto: i32 = v.iter().product(); // 120
// count, min, max
let n = v.iter().count(); // 5
let min = v.iter().min(); // Some(&1)
let max = v.iter().max(); // Some(&5)
// any, all
let tem_par = v.iter().any(|&x| x % 2 == 0); // true
let todos_pos = v.iter().all(|&x| x > 0); // true
// find, position
let primeiro_par = v.iter().find(|&&x| x % 2 == 0); // Some(&2)
let pos_par = v.iter().position(|&x| x % 2 == 0); // Some(1)
// for_each
v.iter().for_each(|x| println!("{}", x));
// partition - divide em dois
let (pares, impares): (Vec<_>, Vec<_>) = v.iter().partition(|&&x| x % 2 == 0);
// unzip
let pares = vec![(1, "a"), (2, "b"), (3, "c")];
let (numeros, letras): (Vec<i32>, Vec<&str>) = pares.into_iter().unzip();
Implementando Iterator
struct Contador {
valor: u32,
maximo: u32,
}
impl Contador {
fn novo(maximo: u32) -> Self {
Contador { valor: 0, maximo }
}
}
impl Iterator for Contador {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.valor < self.maximo {
self.valor += 1;
Some(self.valor)
} else {
None
}
}
}
let soma: u32 = Contador::novo(5).sum(); // 15
String Operations
Criação e Conversão
let s1 = String::new(); // string vazia
let s2 = String::from("olá"); // de &str
let s3 = "olá".to_string(); // de &str
let s4 = String::with_capacity(100); // com capacidade pré-alocada
let s5 = format!("{}-{}-{}", 2024, 1, 15); // formatação
let s6 = ['o', 'l', 'á'].iter().collect::<String>(); // de chars
Manipulação
let mut s = String::from("Olá");
// Adicionar conteúdo
s.push(' '); // adicionar char
s.push_str("Mundo"); // adicionar &str
s += "!"; // concatenar (Add trait)
// Inserir e remover
s.insert(0, '¡'); // inserir char na posição
s.insert_str(0, "-> "); // inserir &str na posição
s.remove(0); // remover char na posição (por byte index)
s.truncate(3); // manter apenas os primeiros 3 bytes
s.clear(); // limpar toda a string
// Concatenação
let a = String::from("Olá");
let b = String::from(" Mundo");
let c = a + &b; // a é movido, b é emprestado
let d = format!("{}{}", c, "!"); // sem mover ninguém
// Repetição
let rep = "ha".repeat(3); // "hahaha"
Busca e Fatiamento
let s = String::from("Olá, Mundo! Bem-vindo ao Rust.");
// Verificações
s.contains("Mundo"); // true
s.starts_with("Olá"); // true
s.ends_with("Rust."); // true
s.is_empty(); // false
s.len(); // tamanho em bytes
s.chars().count(); // tamanho em caracteres
// Busca
s.find("Mundo"); // Some(5) - posição em bytes
s.rfind("o"); // posição da última ocorrência
// Split
let partes: Vec<&str> = s.split(", ").collect();
let palavras: Vec<&str> = s.split_whitespace().collect();
let linhas: Vec<&str> = "a\nb\nc".lines().collect();
let (esq, dir) = s.split_once(", ").unwrap(); // divide em 2
// Trim
let t = " olá ";
t.trim(); // "olá"
t.trim_start(); // "olá "
t.trim_end(); // " olá"
// Substituição
s.replace("Mundo", "Brasil"); // nova String
s.replacen("o", "0", 2); // substitui apenas 2 ocorrências
// Maiúsculas/minúsculas
"olá".to_uppercase(); // "OLÁ"
"OLÁ".to_lowercase(); // "olá"
// Slice (cuidado: por byte index, não char index)
let fatia = &s[0..3]; // "Olá" - CUIDADO com caracteres multibyte
// println! e format!
println!("Inteiro: {}", 42);
println!("Float: {:.2}", 3.14159); // 2 casas decimais
println!("Padding: {:>10}", "dir"); // alinhado à direita
println!("Padding: {:<10}", "esq"); // alinhado à esquerda
println!("Padding: {:^10}", "centro"); // centralizado
println!("Padding: {:0>5}", 42); // "00042"
println!("Debug: {:?}", vec![1, 2, 3]); // formatação Debug
println!("Pretty: {:#?}", vec![1, 2, 3]); // Debug formatado
println!("Binário: {:b}", 42); // "101010"
println!("Hex: {:x}", 255); // "ff"
println!("Hex: {:X}", 255); // "FF"
println!("Octal: {:o}", 8); // "10"
println!("Nomeado: {nome}", nome = "Rust");
Smart Pointers
Box<T> - Alocação no Heap
// Box: ponteiro para dado no heap
let b = Box::new(5);
println!("{}", b); // dereferência automática
// Uso principal: tipos de tamanho desconhecido em tempo de compilação
enum Lista {
Cons(i32, Box<Lista>), // tipo recursivo precisa de Box
Nil,
}
let lista = Lista::Cons(1,
Box::new(Lista::Cons(2,
Box::new(Lista::Cons(3,
Box::new(Lista::Nil))))));
// Trait objects
let animal: Box<dyn std::fmt::Debug> = Box::new("gato");
Rc<T> - Reference Counting (Single-thread)
use std::rc::Rc;
// Rc: múltiplos owners do mesmo dado (imutável, single-thread)
let a = Rc::new(String::from("dados compartilhados"));
let b = Rc::clone(&a); // incrementa o contador
let c = a.clone(); // equivalente
println!("Contagem: {}", Rc::strong_count(&a)); // 3
// Quando b e c saem de escopo, o contador diminui
// Quando chega a 0, o dado é liberado
Arc<T> - Atomic Reference Counting (Multi-thread)
use std::sync::Arc;
use std::thread;
// Arc: como Rc, mas thread-safe (usa contagem atômica)
let dados = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for _ in 0..3 {
let dados_clone = Arc::clone(&dados);
let handle = thread::spawn(move || {
println!("{:?}", dados_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
RefCell<T> - Mutabilidade Interior
use std::cell::RefCell;
// RefCell: permite mutação mesmo com referência imutável
// Verifica regras de borrowing em RUNTIME (não em compilação)
let dados = RefCell::new(vec![1, 2, 3]);
// Empréstimo imutável
let r = dados.borrow();
println!("{:?}", r);
drop(r); // liberar antes de pegar empréstimo mutável
// Empréstimo mutável
dados.borrow_mut().push(4);
// Padrão comum: Rc<RefCell<T>> - múltiplos owners com mutação
use std::rc::Rc;
let compartilhado = Rc::new(RefCell::new(0));
let clone1 = Rc::clone(&compartilhado);
let clone2 = Rc::clone(&compartilhado);
*clone1.borrow_mut() += 10;
*clone2.borrow_mut() += 20;
println!("{}", compartilhado.borrow()); // 30
Mutex<T> - Exclusão Mútua (Multi-thread)
use std::sync::{Arc, Mutex};
use std::thread;
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contador_clone = Arc::clone(&contador);
let handle = thread::spawn(move || {
let mut num = contador_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap()); // 10
Resumo de Smart Pointers
| Tipo | Ownership | Thread-safe | Mutação | Uso |
|---|
Box<T> | Único | Sim* | Sim (se mut) | Alocação no heap |
Rc<T> | Compartilhado | Nao | Nao | Vários owners (single-thread) |
Arc<T> | Compartilhado | Sim | Nao | Vários owners (multi-thread) |
RefCell<T> | Único | Nao | Sim (runtime) | Mutabilidade interior |
Mutex<T> | Compartilhado | Sim | Sim (com lock) | Acesso exclusivo entre threads |
Cell<T> | Único | Nao | Sim (Copy types) | Mutabilidade interior (Copy) |
RwLock<T> | Compartilhado | Sim | Sim (com lock) | Vários leitores OU um escritor |
Async/Await
Fundamentos
// Função assíncrona
async fn buscar_dados(url: &str) -> Result<String, reqwest::Error> {
let resposta = reqwest::get(url).await?;
let texto = resposta.text().await?;
Ok(texto)
}
// Bloco async
let futuro = async {
let resultado = buscar_dados("https://api.exemplo.com").await;
println!("{:?}", resultado);
};
// Executando com tokio
#[tokio::main]
async fn main() {
let dados = buscar_dados("https://api.exemplo.com").await.unwrap();
println!("{}", dados);
}
use tokio;
#[tokio::main]
async fn main() {
// Spawn: executa tarefa em background
let handle = tokio::spawn(async {
// trabalho assíncrono
"resultado"
});
let resultado = handle.await.unwrap();
// join! - executa múltiplas futures concorrentemente
let (r1, r2, r3) = tokio::join!(
buscar_dados("url1"),
buscar_dados("url2"),
buscar_dados("url3"),
);
// select! - espera a primeira future completar
tokio::select! {
val = buscar_dados("url1") => println!("url1: {:?}", val),
val = buscar_dados("url2") => println!("url2: {:?}", val),
}
// Timeout
use tokio::time::{timeout, Duration};
match timeout(Duration::from_secs(5), buscar_dados("url")).await {
Ok(resultado) => println!("{:?}", resultado),
Err(_) => println!("Timeout!"),
}
// Sleep
tokio::time::sleep(Duration::from_millis(100)).await;
}
Streams (Iteradores Assíncronos)
use tokio_stream::StreamExt; // crate tokio-stream
async fn processar_stream() {
let stream = tokio_stream::iter(vec![1, 2, 3, 4, 5]);
// Consumir stream
let mut stream = stream.map(|x| x * 2).filter(|x| *x > 4);
while let Some(valor) = stream.next().await {
println!("{}", valor);
}
}
Channels Assíncronos
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
// Canal com buffer
let (tx, mut rx) = mpsc::channel::<String>(32);
let tx2 = tx.clone();
tokio::spawn(async move {
tx.send("olá".to_string()).await.unwrap();
});
tokio::spawn(async move {
tx2.send("mundo".to_string()).await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("Recebido: {}", msg);
}
}
Macros Úteis
| Macro | Descrição | Exemplo |
|---|
println! | Imprime com nova linha | println!("x = {}", x); |
eprintln! | Imprime no stderr | eprintln!("Erro: {}", e); |
print! | Imprime sem nova linha | print!("Carregando..."); |
format! | Formata e retorna String | let s = format!("{}-{}", a, b); |
dbg! | Debug: imprime expressão e valor | dbg!(x * 2 + 1); |
vec! | Cria Vec | let v = vec![1, 2, 3]; |
todo! | Placeholder (panic com “not yet implemented”) | todo!("implementar depois"); |
unimplemented! | Marca código não implementado | unimplemented!(); |
unreachable! | Marca código inalcançável | unreachable!("nunca deveria chegar aqui"); |
panic! | Encerra com mensagem de erro | panic!("erro fatal"); |
assert! | Verificação (panic se falso) | assert!(x > 0); |
assert_eq! | Igualdade (panic se diferente) | assert_eq!(soma, 42); |
assert_ne! | Desigualdade (panic se igual) | assert_ne!(a, b); |
debug_assert! | Assert apenas em modo debug | debug_assert!(caro_check()); |
cfg! | Verifica config de compilação | if cfg!(target_os = "linux") {} |
include_str! | Inclui arquivo como &str | let sql = include_str!("query.sql"); |
include_bytes! | Inclui arquivo como &[u8] | let img = include_bytes!("logo.png"); |
env! | Variável de ambiente (compilação) | let ver = env!("CARGO_PKG_VERSION"); |
concat! | Concatena literais | let s = concat!("a", "b", "c"); |
stringify! | Converte tokens em &str | let s = stringify!(1 + 2); |
write! / writeln! | Escreve em buffer | write!(buf, "x={}", x)?; |
// dbg! - imprime arquivo, linha, expressão e valor
let x = 5;
let y = dbg!(x * 2) + 1; // [src/main.rs:2] x * 2 = 10
// y = 11
// vec! com repetição
let zeros = vec![0; 100]; // 100 zeros
// assert com mensagem customizada
assert!(x > 0, "x deveria ser positivo, mas era {}", x);
assert_eq!(resultado, esperado, "Resultado inesperado para input {}", input);
Cargo Commands
Comandos Essenciais
| Comando | Descrição |
|---|
cargo new meu_projeto | Criar novo projeto (binário) |
cargo new meu_lib --lib | Criar nova biblioteca |
cargo init | Inicializar projeto no diretório atual |
cargo build | Compilar (modo debug) |
cargo build --release | Compilar (modo release, otimizado) |
cargo run | Compilar e executar |
cargo run -- arg1 arg2 | Executar com argumentos |
cargo test | Rodar todos os testes |
cargo test nome_teste | Rodar testes que contêm o nome |
cargo test -- --nocapture | Testes com stdout visível |
cargo bench | Rodar benchmarks |
cargo doc --open | Gerar e abrir documentação |
cargo check | Verificar sem compilar (mais rápido) |
cargo clean | Limpar artefatos de compilação |
cargo update | Atualizar dependências |
cargo publish | Publicar no crates.io |
cargo clippy | Linter (sugestões de melhorias) |
cargo fmt | Formatar código |
cargo add nome_crate | Adicionar dependência |
cargo remove nome_crate | Remover dependência |
cargo tree | Árvore de dependências |
cargo audit | Verificar vulnerabilidades |
cargo watch -x run | Recompilar ao salvar (precisa instalar) |
Cargo.toml
[package]
name = "meu_projeto"
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome <email@exemplo.com>"]
description = "Descrição do projeto"
license = "MIT"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.11"
[dev-dependencies]
criterion = "0.5"
[build-dependencies]
cc = "1.0"
[profile.release]
opt-level = 3
lto = true
[features]
default = ["feature_a"]
feature_a = []
feature_b = ["dep:alguma_crate"]
[[bin]]
name = "meu_app"
path = "src/main.rs"
[[example]]
name = "exemplo1"
path = "examples/exemplo1.rs"
Estrutura de Projeto
meu_projeto/
├── Cargo.toml
├── Cargo.lock
├── src/
│ ├── main.rs # ponto de entrada (binário)
│ ├── lib.rs # ponto de entrada (biblioteca)
│ ├── modulo.rs # módulo
│ └── modulo/
│ ├── mod.rs # módulo com submódulos
│ └── sub.rs # submódulo
├── tests/
│ └── integracao.rs # testes de integração
├── benches/
│ └── benchmark.rs # benchmarks
├── examples/
│ └── exemplo.rs # exemplos (cargo run --example exemplo)
└── build.rs # build script
Atributos Comuns
Derive
#[derive(Debug)] // habilita {:?}
#[derive(Clone)] // habilita .clone()
#[derive(Copy, Clone)] // cópia implícita (requer Clone)
#[derive(PartialEq, Eq)] // comparação == e !=
#[derive(PartialOrd, Ord)] // comparação <, >, <=, >=
#[derive(Hash)] // hashing
#[derive(Default)] // Default::default()
#[derive(serde::Serialize, serde::Deserialize)] // serialização (crate serde)
Testes
#[test] // marca função como teste
fn teste_soma() {
assert_eq!(2 + 2, 4);
}
#[test]
#[should_panic] // teste deve causar panic
fn teste_divisao_zero() {
dividir(1, 0);
}
#[test]
#[should_panic(expected = "divisão por zero")]
fn teste_com_mensagem() {
dividir(1, 0);
}
#[test]
#[ignore] // ignorar por padrão (cargo test -- --ignored)
fn teste_lento() {
// teste demorado
}
#[cfg(test)] // módulo compilado apenas em modo teste
mod tests {
use super::*;
#[test]
fn teste_interno() {
// pode acessar itens privados do módulo pai
}
}
Compilação Condicional
#[cfg(target_os = "linux")]
fn somente_linux() {}
#[cfg(target_os = "windows")]
fn somente_windows() {}
#[cfg(feature = "avancado")]
fn recurso_avancado() {}
#[cfg(debug_assertions)]
fn somente_debug() {}
#[cfg(test)]
mod testes { /* ... */ }
// Negação
#[cfg(not(target_os = "windows"))]
fn nao_windows() {}
// Combinação
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn linux_64() {}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unix_like() {}
Outros Atributos Úteis
#[allow(dead_code)] // silenciar aviso de código não usado
#[allow(unused_variables)] // silenciar aviso de variável não usada
#[allow(unused_imports)] // silenciar aviso de import não usado
#[warn(missing_docs)] // avisar sobre documentação faltando
#[deny(unsafe_code)] // proibir código unsafe
#[inline] // sugerir inlining ao compilador
#[inline(always)] // forçar inlining
#[inline(never)] // nunca fazer inline
#[must_use] // aviso se o retorno for ignorado
fn calcular() -> i32 { 42 }
#[deprecated(since = "2.0", note = "Use nova_funcao() em vez disso")]
fn funcao_antiga() {}
#[doc = "Documentação da função"]
/// Documentação (equivalente ao #[doc])
fn documentada() {}
#[repr(C)] // layout de memória compatível com C
struct DadosC {
x: i32,
y: i32,
}
#[repr(u8)] // enum com representação u8
enum Bandeira {
A = 0,
B = 1,
C = 2,
}
#[non_exhaustive] // permite adicionar variantes no futuro
enum Versao {
V1,
V2,
}
Módulos
Declaração e Uso
// Definindo módulo inline
mod matematica {
pub fn somar(a: i32, b: i32) -> i32 { a + b }
fn auxiliar() {} // privado por padrão
pub mod avancado {
pub fn potencia(base: i32, exp: u32) -> i32 {
(0..exp).fold(1, |acc, _| acc * base)
}
}
}
// Usando
use matematica::somar;
use matematica::avancado::potencia;
// use com alias
use std::collections::HashMap as Mapa;
// use com glob (evitar em produção)
use std::collections::*;
// Re-exportar
pub use matematica::somar;
// Paths
crate::matematica::somar(1, 2); // caminho absoluto desde a raiz do crate
self::somar(1, 2); // caminho relativo ao módulo atual
super::funcao(); // caminho para o módulo pai
Testes
Testes Unitários e de Integração
// src/lib.rs
pub fn somar(a: i32, b: i32) -> i32 { a + b }
pub fn dividir(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("divisão por zero".to_string())
} else {
Ok(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn teste_somar() {
assert_eq!(somar(2, 3), 5);
}
#[test]
fn teste_somar_negativos() {
assert_eq!(somar(-1, -2), -3);
}
#[test]
fn teste_dividir_ok() {
let resultado = dividir(10.0, 2.0);
assert!(resultado.is_ok());
assert_eq!(resultado.unwrap(), 5.0);
}
#[test]
fn teste_dividir_por_zero() {
let resultado = dividir(10.0, 0.0);
assert!(resultado.is_err());
}
// Teste que retorna Result
#[test]
fn teste_com_result() -> Result<(), String> {
let r = dividir(10.0, 2.0)?;
assert_eq!(r, 5.0);
Ok(())
}
}
// tests/integracao.rs (teste de integração)
use meu_projeto::somar;
#[test]
fn teste_integracao_somar() {
assert_eq!(somar(100, 200), 300);
}
Dicas Rápidas
Conversões Comuns
// Número para String
let s = 42.to_string();
let s = format!("{}", 42);
// String para número
let n: i32 = "42".parse().unwrap();
let n = "42".parse::<f64>().unwrap();
// Vec<u8> para String
let s = String::from_utf8(vec![72, 101, 108, 108, 111]).unwrap();
// String para Vec<u8>
let bytes = "Hello".as_bytes().to_vec();
let bytes: Vec<u8> = "Hello".bytes().collect();
// &str para &[u8]
let bytes: &[u8] = "hello".as_bytes();
// Slice para Vec
let v = [1, 2, 3].to_vec();
let v: Vec<i32> = [1, 2, 3].into();
// Vec para slice
let s: &[i32] = &v;
// Option para Result
let r: Result<i32, &str> = Some(42).ok_or("vazio");
// Result para Option
let o: Option<i32> = Ok(42).ok();
Padrões Idiomáticos
// Builder pattern
struct Config {
host: String,
porta: u16,
timeout: u64,
}
impl Config {
fn novo() -> Self {
Config {
host: "localhost".into(),
porta: 8080,
timeout: 30,
}
}
fn host(mut self, host: &str) -> Self {
self.host = host.into();
self
}
fn porta(mut self, porta: u16) -> Self {
self.porta = porta;
self
}
fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
}
let config = Config::novo()
.host("0.0.0.0")
.porta(3000)
.timeout(60);
// Newtype pattern
struct Email(String);
struct Idade(u8);
// Type alias
type Resultado<T> = Result<T, Box<dyn std::error::Error>>;