DevTools

Cheatsheet Rust

Linguagem de sistemas com segurança de memória sem garbage collector

Voltar às linguagens
Rust
92 cards encontrados
Categorias:
Versões:

Básico e Tipos


11 cards
Variáveis e mutabilidade
let x = 5;        // imutável
let mut y = 10;   // mutável
y = 20;           // OK

// Shadowing (nova variável):
let x = x + 1;    // novo x
let x = x * 2;    // outro x

// Type annotation:
let n: i64 = 42;
let s: &str = "olá";

Em Rust as variáveis são imutáveis por defeito — o compilador impede reatribuição acidental. Use mut só quando necessário. O shadowing cria uma nova variável com o mesmo nome (pode até mudar o tipo).

Strings
// &str (slice imutável):
let s: &str = "Olá Mundo";

// String (heap, mutável):
let mut s = String::from("Olá");
s.push_str(" Mundo");
s.push('!');

// Formatação:
let msg = format!("Olá, {}!", nome);
let n = format!("{:.2}", 3.14159); // "3.14"
let p = format!("{:05}", 42);      // "00042"

// Operações:
s.len();  s.contains("Mundo");
s.replace("Mundo", "Rust");

Rust distingue &str (fatia imutável) de String (buffer mutável no heap). Use &str em parâmetros para aceitar ambos sem alocar. A macro format! cria strings formatadas sem mutação.

Type aliases
// Alias para tipos longos:
type Result<T> = std::result::Result<T, AppError>;
type Ponto = (f64, f64);
type Callback = Box<dyn Fn(i32) -> i32>;

// Uso:
fn origem() -> Ponto {
    (0.0, 0.0)
}

// Não cria tipo novo (é só atalho):
type Metros = f64;
type Segundos = f64;
// Metros e Segundos são intercambiáveis

O type cria um alias (atalho) para um tipo existente — não cria um tipo novo, por isso os tipos são intercambiáveis. É útil para simplificar assinaturas longas como Result<T, AppError> ou callbacks complexos.

Constantes e statics
// const: avaliada em compilação
const MAX: u32 = 100_000;
const PI: f64 = 3.14159;

// static: vive o programa todo
static NOME: &str = "Rust";
static mut CONTADOR: u32 = 0;

// Diferença:
// const é copiada em cada uso
// static tem endereço fixo na memória

O const é avaliado em tempo de compilação e copiado em cada uso. O static vive durante todo o programa e tem um endereço fixo. Use static mut com cuidado (requer unsafe).

Conversões de tipos
// parse (string -> tipo):
let n: i32 = "42".parse().unwrap();

// as (casting numérico):
let y: u8 = 100i32 as u8;
let z: f64 = 42 as f64;

// to_string:
let s = 42.to_string();

// From / Into (infalível):
let s = String::from("olá");

// TryFrom (pode falhar):
use std::convert::TryFrom;
let r = u8::try_from(300); // Err

Rust não faz conversões implícitas — todas são explícitas. O parse() converte strings em números (retorna Result). O as faz casting mas pode truncar. From/Into são infalíveis; TryFrom retorna Result quando pode falhar.

Inferência de tipos
// O compilador infere o tipo:
let x = 5;          // i32 (default)
let f = 3.14;       // f64 (default)
let v = vec![1, 2]; // Vec<i32>

// Inferência pelo uso:
let n = "42".parse().unwrap();
// n: i32? i64? — precisa de contexto
let n: i64 = "42".parse().unwrap();

// Turbofish (::<>):
let n = "42".parse::<i32>().unwrap();
let v = vec![1, 2, 3].iter().collect::<Vec<_>>();

O compilador infere tipos a partir do contexto, mas inteiros e floats têm default (i32, f64). Quando ambíguo, anote o tipo ou use a sintaxe turbofish ::<Tipo> para especificar explicitamente o parâmetro genérico.

Tipos escalares
// Inteiros:
i8, i16, i32, i64, i128, isize
u8, u16, u32, u64, u128, usize

// Float:
let f: f32 = 3.14;
let d: f64 = 3.14159265;

// Bool e Char (Unicode 4 bytes):
let ativo: bool = true;
let emoji: char = '🦀';

// Literais:
let dec = 98_222;
let hex = 0xff;
let bin = 0b1111_0000;

Rust tem inteiros com tamanho explícito (i8 a i128, u8 a u128); isize/usize adaptam-se à arquitetura. O char é um scalar Unicode de 4 bytes. Underscores em literais (98_222) melhoram a legibilidade.

Operadores
// Aritméticos:
+ - * / %
let r = 10 / 3;  // 3 (inteiro)

// Comparação e lógicos:
==  !=  >  <  >=  <=
&&  ||  !

// Bitwise:
&  |  ^  !  <<  >>

// Range:
1..5    // 1,2,3,4
1..=5   // 1,2,3,4,5

// Não há ++ ou -- (use += 1)

A divisão inteira trunca em direção a zero. Os ranges 1..5 (exclusivo) e 1..=5 (inclusivo) são iteradores lazy. Rust não tem ++ nem -- — use += 1 para evitar ambiguidade de precedência.

Tuples e destructuring
let tup = (1, "dois", 3.0);

// Acesso por índice:
let a = tup.0;  // 1
let b = tup.1;  // "dois"

// Destructuring:
let (x, y, z) = tup;

// Ignorar elementos:
let (primeiro, ..) = tup;
let (.., ultimo) = tup;

// Retorno múltiplo de função:
fn dividir(a: f64, b: f64) -> (f64, f64) {
    (a / b, a % b)
}
let (q, r) = dividir(10.0, 3.0);

Tuples agrupam valores de tipos diferentes com acesso por índice (.0, .1). O destructuring extrai tudo de uma vez; .. ignora o resto. São ideais para retornar múltiplos valores de uma função sem criar um struct.

Tipos compostos
// Tuple:
let tup: (i32, f64, char) = (500, 6.4, 'A');
let (x, y, z) = tup;   // destructuring
let primeiro = tup.0;

// Array (tamanho fixo, stack):
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let zeros = [0; 10];   // 10 zeros

// Slice (referência parcial):
let slice = &arr[1..3]; // [2, 3]

// Unit type:
let _unit: () = ();

Tuples agrupam tipos diferentes com acesso por índice (.0) ou destructuring. Arrays têm tamanho fixo na stack. Slices (&arr[1..3]) são views sem copiar dados. O unit type () representa ausência de valor.

Macros básicas
// println! (stdout):
println!("Olá, {}!", nome);
println!("{x} + {y} = {}", x + y);
println!("{:#?}", struct_complexa);

// eprintln! (stderr):
eprintln!("Erro: {}", msg);

// format! (retorna String):
let s = format!("{} anos", idade);

// dbg! (debug):
let x = dbg!(2 + 2);

// assert:
assert!(x > 0);
assert_eq!(a, b);

Macros (com !) são expandidas em compilação. O println! usa {:?} para Debug e {:#?} formatado. O dbg! imprime ficheiro, linha e valor. assert!/assert_eq! causam panic se falharem.

Ownership e Memória


10 cards
Ownership
// Um valor = um dono:
let s1 = String::from("olá");
let s2 = s1;   // s1 movido! inválido
// println!("{s1}"); // ERRO!

// Clone (cópia profunda):
let s2 = s1.clone(); // ambos válidos

// Tipos Copy (stack, copiados):
let x = 5;
let y = x;   // x ainda válido (i32 é Copy)

// Ao sair do scope: drop automático

Ownership é o conceito central de Rust — cada valor tem um dono, e ao sair do scope é libertado (sem GC). Atribuir move o valor (o original fica inválido). Tipos simples na stack (i32, bool) são Copy e não movem.

Rc e Arc
// Rc (múltiplos donos, single-thread):
use std::rc::Rc;
let a = Rc::new(String::from("partilhado"));
let b = Rc::clone(&a);  // ref count = 2
let c = Rc::clone(&a);  // ref count = 3
drop(c);                // ref count = 2

// Arc (atomic, thread-safe):
use std::sync::Arc;
let dados = Arc::new(vec![1, 2, 3]);
let d2 = Arc::clone(&dados);
std::thread::spawn(move || {
    println!("{:?}", d2);
});

Rc (Reference Counting) permite múltiplos donos do mesmo dado — só é libertado quando o contador chega a zero. Não é thread-safe. O Arc (Atomic Rc) é a versão thread-safe, com contagem atómica — necessário para partilhar entre threads.

Drop e RAII
struct Ligacao { nome: String }

impl Drop for Ligacao {
    fn drop(&mut self) {
        println!("A fechar {}", self.nome);
    }
}

{
    let l = Ligacao { nome: "DB".into() };
    // usa a ligação...
}   // drop() chamado automaticamente aqui

// Forçar drop antes:
let l = Ligacao { nome: "DB".into() };
drop(l);   // fecha já
// l não pode ser usada depois

O trait Drop executa código quando um valor sai do scope (RAII — Resource Acquisition Is Initialization). É chamado automaticamente — ideal para fechar ficheiros, ligações e libertar recursos. A função drop() força a libertação antecipada.

Borrowing e referências
// Borrow imutável (&):
fn tamanho(s: &String) -> usize { s.len() }
tamanho(&s1);   // s1 ainda válido

// Borrow mutável (&mut, só 1 por vez):
fn adicionar(s: &mut String) {
    s.push_str(" mundo");
}
adicionar(&mut s1);

// Regras:
// 1. Um &mut OU vários & (nunca ambos)
// 2. Referências devem ser sempre válidas

Borrowing (&) permite referenciar sem tomar ownership. As regras: pode ter um &mut OU vários &, nunca ambos ao mesmo tempo. Isto previne data races e use-after-free em tempo de compilação — segurança de memória sem custo em runtime.

RefCell
use std::cell::RefCell;

// Interior mutability:
let dados = RefCell::new(vec![1, 2, 3]);

// Mutar através de referência imutável:
dados.borrow_mut().push(4);

// Ler:
let r = dados.borrow();
println!("{:?}", *r);

// Regras verificadas em RUNTIME:
// - vários borrow() OU um borrow_mut()
// - violar causa panic (não erro de compilação)

RefCell permite mutação através de uma referência imutável (interior mutability). As regras de borrowing são verificadas em runtime (borrow()/borrow_mut()) — violá-las causa panic. Use quando precisa de mutar dados por detrás de &self.

Stack vs Heap
// Stack (rápido, tamanho fixo):
let x = 42;            // i32 na stack
let p = (1.0, 2.0);    // tuple na stack
let a = [0; 100];      // array na stack

// Heap (Box, Vec, String):
let b = Box::new(42);  // i32 no heap
let v = vec![1, 2, 3]; // dados no heap
let s = String::from("olá");

// Tamanho deve ser conhecido (Sized):
// fn f(x: dyn Resumo) {} // ERRO
fn f(x: &dyn Resumo) {}   // OK (ponteiro)

A stack é rápida mas exige tamanho fixo conhecido em compilação (Sized). Dados de tamanho variável (Vec, String) guardam os dados no heap e um ponteiro na stack. Trait objects precisam de & ou Box porque o tamanho é dinâmico.

Move e Clone
// Move (heap types):
let v1 = vec![1, 2, 3];
let v2 = v1;        // v1 inválido

// Clone (cópia profunda):
let v2 = v1.clone(); // ambos válidos

// Funções também movem:
fn consumir(v: Vec<i32>) { }
consumir(v2);       // v2 movido, inválido

// Borrow para não mover:
fn ler(v: &Vec<i32>) { }
ler(&v1);           // v1 continua válido

Tipos com dados no heap (Vec, String) são movidos por defeito ao atribuir ou passar a funções. O clone() faz uma cópia profunda (custo). Para evitar mover, passe por referência (&) — borrowing.

Cow
use std::borrow::Cow;

fn normalizar(s: &str) -> Cow<str> {
    if s.contains(' ') {
        // Precisa modificar -> aloca
        Cow::Owned(s.replace(' ', "_"))
    } else {
        // Sem modificação -> empresta
        Cow::Borrowed(s)
    }
}

let a = normalizar("sem_espaco"); // Borrowed
let b = normalizar("com espaco"); // Owned

Cow (Clone-On-Write) evita clones desnecessários: se não modificar, empresta (Borrowed); se precisar de mudar, aloca (Owned). É ideal para funções que só às vezes precisam de transformar o input, poupando alocações.

Box (heap)
// Aloca no heap:
let b = Box::new(5);

// Tipos recursivos:
enum Lista {
    Vazia,
    No(i32, Box<Lista>),
}
let lista = Lista::No(1, Box::new(Lista::No(2, Box::new(Lista::Vazia))));

// Trait objects:
let item: Box<dyn Resumo> = Box::new(artigo);

// Grande struct na stack -> heap:
let grande = Box::new(StructEnorme::novo());

Box<T> aloca dados no heap (a stack guarda só o ponteiro). É necessário para tipos recursivos (senão o tamanho seria infinito), trait objects (Box<dyn Trait>) e para mover structs grandes sem copiar.

Interior mutability
struct Cache {
    valor: RefCell<Option<String>>,
}

impl Cache {
    // &self (não &mut) mas muta internamente:
    fn obter(&self) -> String {
        let mut v = self.valor.borrow_mut();
        if v.is_none() {
            *v = Some(self.calcular());
        }
        v.clone().unwrap()
    }
}

Interior mutability permite mutar dados através de &self (referência imutável), usando RefCell, Cell ou Mutex por dentro. É útil para caches, contadores e lazy initialization onde o método é logicamente imutável.

Funções e Closures


10 cards
Definir funções
fn somar(a: i32, b: i32) -> i32 {
    a + b   // sem ; = retorno
}

// Sem retorno (unit):
fn saudar(nome: &str) {
    println!("Olá, {nome}!");
}

// Early return:
fn abs(x: i32) -> i32 {
    if x < 0 { return -x; }
    x
}

fn main() {
    println!("{}", somar(2, 3));
}

Funções declaram tipos de todos os parâmetros (obrigatório). O retorno implícito é a última expressão sem ; — o return é para early exits. Sem ->, a função retorna () (unit).

Funções genéricas
// Trait bound:
fn maior<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

// Where clause:
fn processar<T, U>(t: T, u: U) -> String
where
    T: Display + Clone,
    U: Debug,
{
    format!("{t}: {u:?}")
}

// impl Trait no retorno:
fn criar_iter() -> impl Iterator<Item = i32> {
    (0..10).filter(|x| x % 2 == 0)
}

Generics funcionam com qualquer tipo que satisfaça os trait bounds — sem custo em runtime (monomorfização). A where clause organiza bounds complexos. O impl Trait no retorno esconde o tipo concreto (útil para iteradores e closures).

Funções const
// const fn: avaliável em compilação:
const fn quadrado(x: u32) -> u32 {
    x * x
}

const AREA: u32 = quadrado(10); // 100

// Pode ser usada em runtime também:
let n = quadrado(5); // 25

// Limitações (menos ops permitidos):
const fn max(a: u32, b: u32) -> u32 {
    if a > b { a } else { b }
}

Uma const fn pode ser avaliada em tempo de compilação quando os argumentos são constantes. Também funciona em runtime normalmente. Tem limitações (operações permitidas), mas permite calcular valores para const e arrays.

Closures
// Sintaxe:
let soma = |a, b| a + b;
let dobro = |x: i32| -> i32 { x * 2 };

// Captura o ambiente:
let fator = 3;
let triplo = |x| x * fator;

// Como argumento:
let mut nums = vec![3, 1, 4, 1, 5];
nums.sort_by(|a, b| a.cmp(b));

nums.iter().filter(|&&x| x > 2);

Closures são funções anónimas que capturam variáveis do ambiente. O compilador infere os tipos (anotações opcionais). A sintaxe |params| corpo é compacta e ideal para callbacks em iteradores e ordenação.

Métodos (impl)
struct Circulo { raio: f64 }

impl Circulo {
    // Construtor (método associado):
    fn novo(raio: f64) -> Self {
        Circulo { raio }
    }

    // Borrow (&self):
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.raio.powi(2)
    }

    // Mutável (&mut self):
    fn escalar(&mut self, f: f64) {
        self.raio *= f;
    }
}

Blocos impl associam métodos a structs. O primeiro parâmetro define o receiver: &self (borrow), &mut self (mutação) ou self (consume). Funções sem self são métodos associados chamados com Circulo::novo().

Parâmetros e retornos
// Múltiplos retornos (tuple):
fn dividir(a: f64, b: f64) -> (f64, f64) {
    (a / b, a % b)
}
let (q, r) = dividir(10.0, 3.0);

// Retorno nomeado (não existe):
// Rust não tem named returns

// never type (!):
fn falhar() -> ! {
    panic!("erro fatal");
}

// impl Trait como parâmetro:
fn mostrar(item: impl Display) {
    println!("{item}");
}

Para múltiplos valores de retorno use tuples com destructuring. O never type ! indica funções que nunca retornam (como panic!). O impl Trait como parâmetro é açúcar para um genérico com trait bound.

Closures com move
let dados = vec![1, 2, 3];

// move toma ownership:
let closure = move || {
    println!("{:?}", dados);
};
closure();

// Necessário para threads:
let handle = std::thread::spawn(move || {
    // dados foi movido para a thread
    dados.len()
});

// Sem move: closure só empresta
// (pode causar erro se dados sair do scope)

O move força a closure a tomar ownership dos valores capturados. É essencial quando a closure sobrevive ao scope onde foi criada — por exemplo, ao enviá-la para outra thread com thread::spawn.

Function pointers
fn dobrar(x: i32) -> i32 { x * 2 }

// fn é um tipo (não captura ambiente):
let f: fn(i32) -> i32 = dobrar;
println!("{}", f(5)); // 10

// Como parâmetro:
fn aplicar(v: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
    v.iter().map(|&x| f(x)).collect()
}

// Retornar closure:
fn multiplicador(f: i32) -> impl Fn(i32) -> i32 {
    move |x| x * f
}

O tipo fn (minúsculo) é um function pointer — diferente de closures, não captura ambiente. É útil para passar funções nomeadas sem overhead de generics. Para retornar closures use impl Fn (esconde o tipo anónimo).

Traits funcionais
// Fn (borrow, chama várias vezes):
fn aplicar(f: &dyn Fn(i32) -> i32, x: i32) -> i32 {
    f(x)
}

// FnMut (muta o ambiente):
fn contador(f: &mut dyn FnMut()) {
    f(); f();
}

// FnOnce (consome-se, chama 1x):
fn executar(f: impl FnOnce() -> String) {
    println!("{}", f());
}

// Uso:
let r = aplicar(&|x| x * 2, 5); // 10

Rust tem três traits funcionais: Fn (só lê, chama várias vezes), FnMut (pode mutar variáveis capturadas) e FnOnce (consome-se ao ser chamada). O compilador escolhe qual a closure implementa automaticamente.

Recursão
// Fatorial:
fn fatorial(n: u64) -> u64 {
    if n <= 1 { 1 } else { n * fatorial(n - 1) }
}

// Fibonacci:
fn fib(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => fib(n - 1) + fib(n - 2),
    }
}

// Recursão de cauda (tail):
fn soma(n: u64, acc: u64) -> u64 {
    if n == 0 { acc } else { soma(n - 1, acc + n) }
}

Rust suporta recursão, mas o compilador não garante otimização de cauda. Para grandes profundidades prefira iteração ou recursão de cauda com acumulador. O match é ideal para definir casos base e recursivos de forma clara.

Coleções


11 cards
Vec (vetor)
// Criar:
let mut v: Vec<i32> = vec![1, 2, 3, 4, 5];
let zeros = vec![0; 10];

// Modificar:
v.push(6);
v.pop();          // Option<i32>
v.insert(0, 0);
v.retain(|&x| x > 2);

// Acesso:
v[0];             // panics se fora
v.get(0);         // Option<&i32>
v.first();  v.last();
v.len();  v.is_empty();

// Iterar:
for x in &v { println!("{x}"); }

Vec<T> é a coleção mais usada — array dinâmico no heap. v[i] panics se o índice for inválido; v.get(i) retorna Option (seguro). O retain() filtra in-place sem alocar novo vetor.

String operations
let mut s = String::from("Olá Mundo");

// Adicionar:
s.push('!');
s.push_str(" Rust");

// Transformar:
s.to_uppercase();
s.replace("Mundo", "Rust");
s.trim().to_string();

// Split:
let partes: Vec<&str> = s.split(' ').collect();
let linhas: Vec<&str> = texto.lines().collect();

// Verificar:
s.contains("Mundo");
s.starts_with("Olá");
s.len();           // bytes!
s.chars().count(); // caracteres

String em Rust é UTF-8 — len() retorna bytes, não caracteres (um emoji pode ser 4 bytes). Para contar caracteres use chars().count(). O split() retorna um iterador de &str zero-copy. lines() divide por linhas.

VecDeque
use std::collections::VecDeque;

let mut fila = VecDeque::new();

// Inserir nas duas pontas:
fila.push_back(1);   // [1]
fila.push_back(2);   // [1, 2]
fila.push_front(0);  // [0, 1, 2]

// Remover das duas pontas:
fila.pop_front();    // Some(0)
fila.pop_back();     // Some(2)

// Acesso:
fila.front();  fila.back();
fila.get(0);   fila.len();

VecDeque é uma fila de dupla entrada (deque) — inserção e remoção O(1) em ambas as pontas. Use quando precisa de uma fila (FIFO) ou pilha com acesso às duas extremidades. O Vec só é eficiente a inserir/remover no fim.

HashMap
use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert("Ana", 95);
scores.insert("Rui", 87);

// Acesso:
let v = scores.get("Ana"); // Option<&i32>

// Entry API:
scores.entry("Eva").or_insert(100);
scores.entry("Ana").and_modify(|v| *v += 5);

// Iterar:
for (k, v) in &scores {
    println!("{k}: {v}");
}

scores.contains_key("Ana");
scores.remove("Rui");

HashMap oferece lookup O(1) por chave. A API entry() é idiomática: or_insert() insere se não existir, and_modify() atualiza se existir — evita double-lookup. O get() retorna Option; a ordem de iteração não é determinística.

HashSet e BTreeMap
use std::collections::{HashSet, BTreeMap};

// HashSet (únicos, sem ordem):
let mut set = HashSet::new();
set.insert(1); set.insert(2); set.insert(2);
set.len();        // 2
set.contains(&1); // true

// Operações de conjuntos:
let uniao: HashSet<_> = a.union(&b).collect();
let inter: HashSet<_> = a.intersection(&b).collect();

// BTreeMap (ordenado por chave):
let mut bt = BTreeMap::new();
bt.insert("banana", 2);
bt.insert("apple", 1);
// Itera em ordem: apple, banana

HashSet garante unicidade com O(1) lookup e suporta operações de conjuntos (união, interseção, diferença). BTreeMap/BTreeSet mantêm os elementos ordenados (O(log n)). Escolha HashMap para performance, BTree quando a ordem importa.

BinaryHeap
use std::collections::BinaryHeap;

let mut heap = BinaryHeap::new();
heap.push(3);
heap.push(1);
heap.push(4);

// Max-heap (maior primeiro):
heap.peek();  // Some(&4)
heap.pop();   // Some(4)
heap.pop();   // Some(3)

// Min-heap com Reverse:
use std::cmp::Reverse;
let mut min = BinaryHeap::new();
min.push(Reverse(3));
min.push(Reverse(1));
min.pop();    // Some(Reverse(1))

BinaryHeap é uma fila de prioridade (max-heap por defeito — o maior sai primeiro). peek() vê o topo sem remover, pop() remove-o. Para um min-heap, envolva os valores em Reverse. Útil para top-K e agendamento.

Iterators - adaptadores
let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];

// map / filter:
let dobros: Vec<_> = nums.iter().map(|&x| x * 2).collect();
let pares: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// filter_map (Option):
let validos: Vec<_> = dados.iter()
    .filter_map(|s| s.parse::<i32>().ok()).collect();

// flat_map / zip / chain:
let flat: Vec<_> = palavras.iter().flat_map(|s| s.chars()).collect();
let pares: Vec<_> = a.iter().zip(b.iter()).collect();
let all: Vec<_> = v1.iter().chain(v2.iter()).collect();

// take / skip:
nums.iter().take(3);
nums.iter().skip(2);

Adaptadores são lazy — não executam até serem consumidos. O map transforma, o filter seleciona, o filter_map combina ambos para Option. O flat_map achata resultados, o zip emparelha e o chain concatena. Zero-cost.

Slices e windows
let dados = vec![1, 2, 3, 4, 5, 6, 7, 8];

// Slices:
let slice = &dados[2..5]; // [3, 4, 5]

// chunks:
for chunk in dados.chunks(3) {
    println!("{:?}", chunk); // [1,2,3] [4,5,6] [7,8]
}

// windows (sliding):
for w in dados.windows(3) {
    println!("{:?}", w); // [1,2,3] [2,3,4] ...
}

// split por predicado:
let v = vec![1, 0, 2, 0, 3];
for parte in v.split(|&x| x == 0) {
    println!("{:?}", parte);
}

Slices (&vec[2..5]) são views zero-copy. O chunks() divide em blocos de tamanho fixo (o último pode ser menor); o windows() cria janelas deslizantes sobrepostas (útil para médias móveis). O split() divide por um predicado.

Iteradores customizados
struct Fibonacci { a: u64, b: u64 }

impl Iterator for Fibonacci {
    type Item = u64;
    fn next(&mut self) -> Option<u64> {
        let proximo = self.a + self.b;
        self.a = self.b;
        self.b = proximo;
        Some(self.a)
    }
}

let fibs = Fibonacci { a: 0, b: 1 };
let primeiros: Vec<_> = fibs.take(10).collect();

Implemente o trait Iterator (com type Item e next()) para criar iteradores customizados. Ganha de graça todos os adaptadores (map, filter, take...). Retorne None para terminar (ou use iteradores infinitos).

Iterators - consumo
let nums = vec![5, 2, 8, 1, 9, 3];

// collect / fold:
let v: Vec<_> = nums.iter().collect();
let soma = nums.iter().fold(0, |acc, &x| acc + x);
let soma2: i32 = nums.iter().sum();

// Pesquisa:
nums.iter().find(|&&x| x > 5);     // Option
nums.iter().position(|&x| x == 8); // Option<usize>
nums.iter().any(|&x| x > 8);       // bool
nums.iter().all(|&x| x > 0);       // bool

// Extremos e partition:
nums.iter().min();  nums.iter().max();
let (pares, impares): (Vec<_>, Vec<_>) =
    nums.iter().partition(|&&x| x % 2 == 0);

Consumidores terminam a cadeia e produzem um resultado. O collect() converte para Vec, HashMap, String, etc. O fold é o reduce geral. find/position retornam Option. O partition divide em dois grupos num só passe.

Ordenação
let mut v = vec![3, 1, 4, 1, 5, 9];

// Ordem natural:
v.sort();              // [1,1,3,4,5,9]

// Descrescente:
v.sort_by(|a, b| b.cmp(a));

// Por chave:
let mut pessoas = vec![ana, rui, eva];
pessoas.sort_by_key(|p| p.idade);

// Mais rápido (não estável):
v.sort_unstable();

// Remover duplicados consecutivos:
v.sort();
v.dedup();

O sort() é estável (preserva a ordem de iguais); o sort_unstable() é mais rápido mas não preserva. O sort_by_key() ordena por uma chave extraída. O dedup() remove duplicados consecutivos — ordene antes para remover todos.

Erros e Result


9 cards
Result e ?
use std::num::ParseIntError;

fn ler_numero(s: &str) -> Result<i32, ParseIntError> {
    let n = s.trim().parse::<i32>()?;
    Ok(n * 2)
}

// ? propaga o erro (early return Err)
fn pipeline(s: &str) -> Result<i32, ParseIntError> {
    let a = ler_numero(s)?;
    let b = ler_numero("10")?;
    Ok(a + b)
}

Rust não tem exceções — usa Result<T, E> para erros recuperáveis. O operador ? propaga o erro automaticamente (early return com Err) ou extrai o valor em caso de Ok. Torna o código de erro limpo e explícito.

unwrap_or e combinators
// Defaults:
let n = "abc".parse::<i32>().unwrap_or(0);
let n = "abc".parse().unwrap_or_else(|_| 0);

// Encadear operações:
let resultado = "42".parse::<i32>()
    .map(|n| n * 2)
    .map(|n| n + 1)
    .unwrap_or(0);   // 85

// and_then (operações que retornam Option):
let n = Some("42")
    .and_then(|s| s.parse::<i32>().ok())
    .map(|n| n * 2);

Combinators encadeiam operações sem match verboso. O map transforma o valor, o and_then encadeia operações que também retornam Option/Result. O unwrap_or_else calcula o default preguiçosamente só se necessário.

Recuperação de erros
use std::fs::{self, File};
use std::io::ErrorKind;

// match no tipo de erro:
let f = match File::open("dados.txt") {
    Ok(f) => f,
    Err(e) if e.kind() == ErrorKind::NotFound => {
        File::create("dados.txt").unwrap()
    }
    Err(e) => panic!("erro: {e}"),
};

// or_else (lazy):
let f = File::open("dados.txt")
    .or_else(|e| File::create("dados.txt"));

Para recuperar de erros, use match inspecionando o ErrorKind (ex: NotFound) e tome uma ação alternativa (como criar o ficheiro). O or_else oferece uma alternativa preguiçosa em caso de Err, mantendo o encadeamento funcional.

Option
// Option: valor pode não existir
fn encontrar(v: &[i32], alvo: i32) -> Option<usize> {
    v.iter().position(|&x| x == alvo)
}

match encontrar(&dados, 5) {
    Some(i) => println!("Índice {i}"),
    None => println!("Não encontrado"),
}

// Métodos úteis:
valor.unwrap_or(0);
valor.unwrap_or_else(|| calcular());
valor.map(|x| x * 2);
valor.is_some();  valor.is_none();

Option<T> (Some/None) substitui o null — o compilador obriga a tratar a ausência. O unwrap_or dá um default, o map transforma o valor presente. Métodos como is_some()/is_none() verificam o estado.

anyhow
// anyhow = erros dinâmicos (fácil)
use anyhow::{Result, Context};

fn ler_config() -> Result<String> {
    let s = std::fs::read_to_string("config.toml")
        .context("falha ao ler config")?;
    Ok(s)
}

fn main() -> Result<()> {
    let cfg = ler_config()?;
    println!("{cfg}");
    Ok(())
}

O crate anyhow fornece um tipo de erro dinâmico (anyhow::Result) ideal para aplicações — aceita qualquer erro via ?. O .context() adiciona mensagens ao erro. Use em binários; em bibliotecas prefira erros tipados.

Custom errors
#[derive(Debug)]
enum AppError {
    NotFound(String),
    InvalidInput(String),
    Io(std::io::Error),
}

// Conversão automática (para o ?):
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self {
        AppError::Io(e)
    }
}

fn ler() -> Result<String, AppError> {
    let s = std::fs::read_to_string("f.txt")?;
    Ok(s)
}

Defina erros como enums com variantes para cada caso. Implemente From<E> para conversão automática — assim o ? converte erros de bibliotecas no seu tipo. Implemente Display e Error para integração com o ecossistema.

thiserror
// thiserror = erros tipados (bibliotecas)
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("não encontrado: {0}")]
    NotFound(String),

    #[error("erro de IO")]
    Io(#[from] std::io::Error),
}

fn buscar(id: u32) -> Result<(), AppError> {
    Err(AppError::NotFound(format!("item {id}")))
}

O crate thiserror gera as implementações de Error e Display automaticamente via derive. O #[from] cria a conversão From para o ?. É a escolha padrão para bibliotecas que expõem erros tipados aos consumidores.

panic e unwrap
// panic! (erro irrecuperável):
panic!("estado impossível");

// unwrap (panic se None/Err):
let n = "42".parse::<i32>().unwrap();
let v = Some(5).unwrap();

// expect (panic com mensagem):
let n = "42".parse::<i32>()
    .expect("deveria ser número");

// Índices fora do vetor = panic:
let v = vec![1, 2, 3];
// let x = v[10]; // panic!
let x = v.get(10); // None (seguro)

panic! é para erros irrecuperáveis (aborta a thread). O unwrap() faz panic se for None/Err — use só em protótipos ou quando tem certeza. O expect() é igual mas com mensagem descritiva. Prefira get() a indexação direta.

main com Result
use std::fs;

// main pode retornar Result:
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let conteudo = fs::read_to_string("dados.txt")?;
    println!("{conteudo}");
    Ok(())
}

// Se der erro, imprime e sai com código 1
// Equivalente a try! em cada linha

A função main pode retornar Result — se for Err, o programa imprime o erro e termina com código 1. Isto permite usar ? no topo do programa sem match manual. Use Box<dyn Error> para aceitar qualquer erro.

Controlo de Fluxo


10 cards
If / Else
// if é expressão (retorna valor):
let x = if condicao { 10 } else { 20 };

// Cadeia if/else if/else:
let msg = if nota >= 18 {
    "Excelente"
} else if nota >= 10 {
    "Aprovado"
} else {
    "Reprovado"
};

// Sem parênteses na condição:
if x > 0 {
    println!("Positivo");
}

Em Rust o if é uma expressão que retorna um valor — pode ser atribuído diretamente, eliminando o operador ternário. Não usa parênteses na condição. Todos os ramos devem retornar o mesmo tipo.

Loops
// loop (infinito, retorna valor):
let r = loop {
    contador += 1;
    if contador == 10 {
        break contador * 2;
    }
};

// while:
let mut n = 5;
while n > 0 {
    n -= 1;
}

// for (iterador):
for i in 0..5 {
    println!("{i}");
}

// enumerate:
for (i, v) in lista.iter().enumerate() {
    println!("{i}: {v}");
}

Rust tem três loops: loop (infinito, pode retornar valor via break), while (condição) e for (consome iteradores — o mais comum). O for nunca tem off-by-one. O enumerate() dá índice e valor.

Ranges
// Range exclusivo:
for i in 0..5 { }   // 0,1,2,3,4

// Range inclusivo:
for i in 0..=5 { }  // 0,1,2,3,4,5

// Em slices:
let v = vec![1, 2, 3, 4, 5];
let meio = &v[1..4];   // [2,3,4]
let fim  = &v[3..];    // [4,5]

// Como iterador:
let soma: i32 = (1..=100).sum();  // 5050
let pares: Vec<_> = (0..10)
    .step_by(2).collect(); // [0,2,4,6,8]

Os ranges 0..5 (exclusivo) e 0..=5 (inclusivo) são iteradores lazy. Usam-se em for, slicing e métodos como sum(). O step_by() define o passo da iteração.

Match
// Deve ser exaustivo:
let msg = match numero {
    1 => "um",
    2 => "dois",
    3..=5 => "três a cinco",
    _ => "outro",  // wildcard
};

// Destructuring:
match ponto {
    (0, 0) => println!("Origem"),
    (x, 0) => println!("Eixo X: {x}"),
    (x, y) => println!("({x}, {y})"),
}

// match retorna valor

O match é o pattern matching de Rust — mais poderoso que switch. O compilador exige exaustividade (todos os casos ou _), prevenindo casos esquecidos. Faz destructuring automático de tuples, structs e enums.

Labels e controlo
// Loop labels:
'outer: for i in 0..5 {
    for j in 0..5 {
        if i * j > 12 {
            break 'outer;
        }
        if j % 2 == 0 {
            continue 'outer;
        }
    }
}

// break com valor em loop aninhado:
let r = 'calc: loop {
    for x in dados.iter() {
        if *x > 100 { break 'calc *x; }
    }
    break 0;
};

Labels ('outer:) identificam loops para break/continue em loops aninhados — sem labels, só afeta o loop mais interno. Combinado com break com valor, retorna resultados de loops aninhados sem flags booleanas.

Result em match
// Result: Ok(valor) ou Err(erro)
match "42".parse::<i32>() {
    Ok(n) => println!("Número: {n}"),
    Err(e) => println!("Erro: {e}"),
}

// if let com Result:
if let Ok(n) = input.parse::<i32>() {
    println!("Válido: {n}");
}

// unwrap_or (default):
let n = "abc".parse::<i32>().unwrap_or(0);

// map (transforma Ok):
let dobro = "21".parse::<i32>()
    .map(|n| n * 2);  // Ok(42)

O Result (Ok/Err) substitui exceções. O if let Ok trata só o sucesso. O unwrap_or dá um valor por defeito em caso de erro. O map transforma o valor dentro do Ok sem tocar no erro.

Match guards
// Guard com if:
match x {
    n if n < 0 => println!("Negativo"),
    0 => println!("Zero"),
    n => println!("Positivo: {n}"),
}

// Guard com padrões:
match ponto {
    (x, y) if x == y => println!("Diagonal"),
    (x, y) => println!("({x}, {y})"),
}

// Bindings:
match valor {
    n @ 1..=10 => println!("Pequeno: {n}"),
    n => println!("Grande: {n}"),
}

Os match guards (if após o padrão) adicionam condições arbitrárias a cada braço. O binding n @ padrão dá um nome à variável enquanto testa o padrão — útil para ranges com nome.

Destructuring patterns
// Structs:
let Ponto { x, y } = ponto;
let Ponto { x: px, y: py } = ponto;
let Ponto { x, .. } = ponto; // ignora y

// Enums:
match msg {
    Mensagem::Mover { x, y } => { }
    Mensagem::Escrever(s) => { }
    Mensagem::Cor(r, g, b) => { }
}

// Referências e aninhados:
let &(a, b) = &(1, 2);
match valor {
    Some(Some(x)) => println!("{x}"),
    _ => {},
}

O destructuring extrai valores de structs e enums diretamente. O padrão .. ignora campos não necessários. Pode renomear com x: px. Padrões aninhados (Some(Some(x))) desembrulham várias camadas de Option numa linha.

Option e if let
// Option: Some(valor) ou None
match valor {
    Some(v) => println!("Tem: {v}"),
    None => println!("Vazio"),
}

// if let (só um padrão):
if let Some(v) = valor {
    println!("Tem: {v}");
}

// while let:
while let Some(item) = stack.pop() {
    println!("{item}");
}

// let-else (Rust 1.65+):
let Some(v) = valor else {
    return;
};

O Option (Some/None) substitui o null. O if let é açúcar para um match com um só padrão. O while let itera até o padrão falhar. O let-else extrai o valor ou faz early return.

Expressões e blocos
// Blocos são expressões:
let x = {
    let a = 3;
    let b = 4;
    a + b   // sem ; = retorno
};

// if/match/loop como expressão:
let max = if a > b { a } else { b };
let abs = match x {
    n if n < 0 => -n,
    n => n,
};
let val = loop { break 42; };

Rust é expression-based: quase tudo retorna um valor. A última linha de um bloco sem ; é o valor retornado. Isto elimina variáveis mutáveis temporárias — usa-se if/match/loop como expressão. O ; descarta o valor.

Structs, Enums e Traits


11 cards
Structs
// Named fields:
struct User {
    nome: String,
    idade: u32,
    ativo: bool,
}

let user = User {
    nome: String::from("Ana"),
    idade: 30,
    ativo: true,
};

// Field init shorthand:
fn criar(nome: String, idade: u32) -> User {
    User { nome, idade, ativo: true }
}

// Struct update:
let u2 = User { nome: "Rui".into(), ..user };

Structs são o principal mecanismo de composição de dados (não há classes). O field init shorthand evita repetição quando a variável tem o mesmo nome do campo. O spread ..user copia os campos não especificados de outra instância.

Traits
// Definir trait:
trait Resumo {
    fn resumir(&self) -> String;

    // Default method:
    fn preview(&self) -> String {
        format!("{}...", &self.resumir()[..20])
    }
}

// Implementar:
impl Resumo for Artigo {
    fn resumir(&self) -> String {
        format!("{} por {}", self.titulo, self.autor)
    }
}

// Trait bound:
fn notificar(item: &impl Resumo) {
    println!("{}", item.resumir());
}

Traits definem comportamento partilhado (como interfaces, mas mais poderosas). Podem ter default methods com implementação. Use impl Trait em parâmetros para dispatch estático (zero custo) ou dyn Trait para dinâmico.

Operator overloading
use std::ops::Add;

#[derive(Debug, Clone, Copy)]
struct Ponto(f64, f64);

impl Add for Ponto {
    type Output = Ponto;
    fn add(self, other: Ponto) -> Ponto {
        Ponto(self.0 + other.0, self.1 + other.1)
    }
}

let p = Ponto(1.0, 2.0) + Ponto(3.0, 4.0);
// Ponto(4.0, 6.0)

Operator overloading é feito implementando traits de std::ops (Add, Sub, Mul...). O type Output define o tipo do resultado. Permite usar +, -, * com tipos customizados de forma natural.

Tuple e unit structs
// Tuple struct:
struct Ponto(f64, f64);
let p = Ponto(3.0, 4.0);
let x = p.0;

// Unit struct:
struct Marcador;

// Newtype pattern:
struct Metros(f64);
struct Segundos(f64);

fn velocidade(d: Metros, t: Segundos) -> f64 {
    d.0 / t.0
}

Tuple structs criam tipos distintos com a mesma estrutura — Ponto(f64,f64) é diferente de Cor(f64,f64). Unit structs não têm dados (úteis como marcadores). O newtype pattern evita confundir tipos com a mesma representação.

Trait objects (dyn)
// Coleção heterogénea:
let itens: Vec<Box<dyn Resumo>> = vec![
    Box::new(artigo),
    Box::new(tweet),
];

for item in &itens {
    println!("{}", item.resumir());
}

// Parâmetro dinâmico:
fn mostrar_tudo(lista: &[Box<dyn Resumo>]) {
    for item in lista {
        println!("{}", item.preview());
    }
}

Trait objects (dyn Trait) permitem polimorfismo dinâmico via vtable — útil para coleções heterogéneas (Vec<Box<dyn Trait>>). Têm um pequeno custo em runtime; prefira generics (impl Trait) quando possível.

Enums como state machines
enum Estado {
    Rascunho { conteudo: String },
    Revisao { conteudo: String },
    Publicado { conteudo: String },
}

impl Estado {
    fn submeter(self) -> Result<Self, String> {
        match self {
            Estado::Rascunho { conteudo } =>
                Ok(Estado::Revisao { conteudo }),
            _ => Err("Só rascunhos".into()),
        }
    }
}

O typestate pattern usa enums para modelar máquinas de estados onde transições inválidas são impossíveis em compilação. Os métodos consomem self (ownership) e retornam um novo estado — o antigo deixa de existir.

Enums
enum Mensagem {
    Sair,
    Mover { x: i32, y: i32 },
    Escrever(String),
    Cor(u8, u8, u8),
}

impl Mensagem {
    fn processar(&self) {
        match self {
            Mensagem::Sair => println!("Sair"),
            Mensagem::Mover { x, y } => println!("({x},{y})"),
            Mensagem::Escrever(s) => println!("{s}"),
            Mensagem::Cor(r, g, b) => println!("#{r:02x}{g:02x}{b:02x}"),
        }
    }
}

Enums em Rust são sum types (tagged unions) — cada variante pode ter dados diferentes. Combinados com match, garantem exaustividade. Option e Result são enums genéricos que tornam estados inválidos irrepresentáveis.

Derive
// Derive comum:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Config {
    host: String,
    porta: u16,
}

// Traits deriváveis:
// Debug, Clone, Copy, PartialEq, Eq
// PartialOrd, Ord, Hash, Default

// Uso:
let c = Config { host: "localhost".into(), porta: 8080 };
println!("{:?}", c);   // Debug
let c2 = c.clone();    // Clone
assert_eq!(c, c2);     // PartialEq

O #[derive] gera implementações automáticas. Debug permite {:?}, Clone permite .clone(), PartialEq/Eq permitem ==. Copy é para tipos simples copiados por bits. Display não é derivável.

Lifetimes
// Lifetime em referências:
fn mais_longo<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}

// Em structs:
struct Excerto<'a> {
    texto: &'a str,
}

// 'static:
let s: &'static str = "vive para sempre";

// Elision (não precisa anotar):
fn primeiro(s: &str) -> &str { &s[..1] }

Lifetimes ('a) garantem que referências não sobrevivem aos dados que apontam — previnem dangling pointers sem GC. As elision rules inferem lifetimes na maioria dos casos. Só anote quando houver ambiguidade. 'static vive o programa todo.

Generics em structs
struct Ponto<T> {
    x: T,
    y: T,
}

impl<T: std::fmt::Display> Ponto<T> {
    fn mostrar(&self) {
        println!("({}, {})", self.x, self.y);
    }
}

// Só para f64:
impl Ponto<f64> {
    fn distancia(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Structs genéricos funcionam com qualquer tipo — o compilador gera código especializado (monomorfização, zero custo). Pode restringir com trait bounds no impl e ter impls específicos para tipos concretos (Ponto<f64>).

Associated types
trait Iterador {
    type Item;
    fn proximo(&mut self) -> Option<Self::Item>;
}

struct Contador { atual: u32 }

impl Iterador for Contador {
    type Item = u32;
    fn proximo(&mut self) -> Option<u32> {
        self.atual += 1;
        Some(self.atual)
    }
}

Associated types (type Item) definem tipos dependentes dentro da trait — mais ergonómico que generics extra. Cada implementação fixa o tipo concreto. É o padrão usado pelo trait Iterator da std (Item).

Concorrência e Async


10 cards
Threads
use std::thread;

// Spawn:
let handle = thread::spawn(|| {
    println!("Noutra thread!");
});
handle.join().unwrap(); // espera

// move (toma ownership):
let dados = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("{:?}", dados);
});

// Várias threads:
let handles: Vec<_> = (0..5)
    .map(|i| thread::spawn(move || i * 2))
    .collect();

thread::spawn cria uma thread OS e retorna um JoinHandle. O join() espera a thread terminar e retorna o resultado. Use move para transferir ownership dos dados para a thread. O compilador rejeita código com potenciais data races.

Atómicos
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;

let contador = Arc::new(AtomicUsize::new(0));

for _ in 0..10 {
    let c = Arc::clone(&contador);
    thread::spawn(move || {
        c.fetch_add(1, Ordering::SeqCst);
    });
}

// Sem Mutex (mais leve):
println!("{}", contador.load(Ordering::SeqCst));

Tipos atómicos (AtomicUsize, AtomicBool...) permitem operações seguras entre threads sem Mutex — mais leves para contadores e flags. O fetch_add incrementa atomicamente. O Ordering controla a consistência de memória.

Thread pool (rayon)
// rayon = paralelismo de dados fácil
use rayon::prelude::*;

let v: Vec<i32> = (0..1_000_000).collect();

// par_iter (paralelo automático):
let soma: i64 = v.par_iter()
    .map(|&x| x as i64 * x as i64)
    .sum();

// Ordenação paralela:
let mut dados = vec![3, 1, 4, 1, 5];
dados.par_sort();

// filter paralelo:
let pares: Vec<_> = v.par_iter()
    .filter(|&&x| x % 2 == 0)
    .collect();

O crate rayon oferece paralelismo de dados com quase zero esforço — troque iter() por par_iter() e o trabalho é dividido automaticamente pelas cores da CPU. Ideal para CPU-bound (map, filter, sort em grandes coleções).

Channels (mpsc)
use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send("olá").unwrap();
});

let msg = rx.recv().unwrap(); // bloqueia
println!("{msg}");

// Múltiplos produtores:
let tx2 = tx.clone();

// Iterar mensagens:
for msg in rx {
    println!("{msg}");
}

Channels seguem o princípio "partilhe memória comunicando". O mpsc (multiple producer, single consumer) tem tx (envio) e rx (receção). O send() move o valor (ownership). O recv() bloqueia até haver mensagem; iterar em rx termina quando todos os tx caem.

Async/Await
// async fn retorna um Future (lazy):
async fn fetch_dados(url: &str) -> Result<String, reqwest::Error> {
    let body = reqwest::get(url).await?.text().await?;
    Ok(body)
}

// .await executa o future:
async fn processar() {
    let dados = fetch_dados("https://api.exemplo.com")
        .await.unwrap();
    println!("{dados}");
}

async fn retorna um Future (lazy — não executa até .await). A sintaxe parece síncrona mas a execução é não-bloqueante — ideal para I/O intensivo (HTTP, DB). Precisa de um runtime (como tokio) para executar os futures.

Scoped threads
use std::thread;

let dados = vec![1, 2, 3, 4, 5];
let mut resultados = vec![];

// scope permite borrow sem Arc:
thread::scope(|s| {
    for chunk in dados.chunks(2) {
        s.spawn(|| {
            // empresta chunk e resultados
            let soma: i32 = chunk.iter().sum();
            println!("soma: {soma}");
        });
    }
}); // todas as threads terminam aqui

thread::scope cria threads que podem emprestar (&) dados do scope envolvente sem Arc nem move — o compilador garante que todas terminam antes dos dados saírem do scope. Mais simples e seguro para paralelismo com dados locais.

Arc e Mutex
use std::sync::{Arc, Mutex};
use std::thread;

let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let c = Arc::clone(&contador);
    handles.push(thread::spawn(move || {
        let mut val = c.lock().unwrap();
        *val += 1;
    }));
}

for h in handles { h.join().unwrap(); }
println!("{}", *contador.lock().unwrap()); // 10

Arc (Atomic Rc) partilha dados entre threads; Mutex garante acesso exclusivo (uma thread de cada vez via lock()). Juntos permitem estado partilhado mutável seguro. O lock() retorna um guard que liberta o mutex ao sair do scope.

Tokio runtime
// Cargo.toml:
// tokio = { version = "1", features = ["full"] }

#[tokio::main]
async fn main() {
    let resultado = processar().await;
}

// Concorrência com join!:
let (a, b) = tokio::join!(
    fetch_dados("url1"),
    fetch_dados("url2")
);

// Spawn de tasks:
let handle = tokio::spawn(async {
    trabalho_pesado().await
});
let r = handle.await.unwrap();

O tokio é o runtime async padrão — gere milhares de tasks concorrentes numa pool de threads OS. O join! executa futures em paralelo e espera todos. O tokio::spawn cria tasks independentes (mais leves que threads). O atributo #[tokio::main] configura o runtime.

Send e Sync
// Send: pode mover entre threads
// Sync: pode partilhar (&T) entre threads

// Maioria dos tipos é Send + Sync:
// i32, String, Vec<T>, Arc<T>

// Rc NÃO é Send nem Sync:
// let r = Rc::new(5);
// thread::spawn(move || r); // ERRO

// Mutex<T> é Sync, RefCell NÃO:
// (Mutex verifica em runtime, RefCell não é thread-safe)

// O compilador verifica automaticamente

Send marca tipos que podem ser movidos entre threads; Sync os que podem ser partilhados por referência. A maioria é ambos. Rc não é nenhum (use Arc); RefCell não é Sync (use Mutex). O compilador verifica tudo automaticamente.

Async concorrente
use futures::future::join_all;

// Várias tasks em paralelo:
async fn buscar_todos(urls: Vec<String>) -> Vec<String> {
    let futures = urls.iter()
        .map(|u| fetch_dados(u));
    join_all(futures).await
        .into_iter()
        .filter_map(|r| r.ok())
        .collect()
}

// select! (primeiro a terminar):
tokio::select! {
    r = operacao_a() => println!("A: {r}"),
    r = operacao_b() => println!("B: {r}"),
}

O join_all executa vários futures em concurrently e espera todos. O select! espera o primeiro a terminar (corrida) — útil para timeouts e cancelamento. Esta abordagem escala a milhares de operações I/O sem criar threads OS por cada uma.

Cargo e Ferramentas


10 cards
Cargo
# Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

# Comandos:
cargo new projeto      # criar
cargo build            # compilar
cargo run              # compilar + executar
cargo test             # testes
cargo add serde        # adicionar dep
cargo build --release  # otimizado

Cargo é o build system e gestor de pacotes — compila, gere dependências, corre testes e publica crates. As dependências ficam no Cargo.toml. O --release ativa otimizações. O cargo add adiciona dependências automaticamente.

Testes
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_somar() {
        assert_eq!(somar(2, 3), 5);
    }

    #[test]
    #[should_panic(expected = "erro")]
    fn test_falha() {
        panic!("erro esperado");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        if 2 + 2 == 4 { Ok(()) }
        else { Err("matemática falhou".into()) }
    }
}

Testes ficam em #[cfg(test)] (só compilados em teste). O #[test] marca funções de teste. assert_eq!/assert! verificam condições. #[should_panic] testa panics esperados. Testes podem retornar Result para usar ?.

Features e perfis
# Cargo.toml - features opcionais:
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml"]

# Uso no código:
#[cfg(feature = "json")]
fn parse_json() { }

# Perfis de build:
[profile.release]
opt-level = 3
lto = true
strip = true

Features são funcionalidades opcionais ativáveis (--features full) — permitem dependências condicionais. Os perfis controlam otimização: dev (rápido a compilar) e release (otimizado). opt-level, lto e strip afinam o binário final.

Módulos
// src/main.rs ou src/lib.rs
mod utils {
    pub fn helper() { }
    pub(crate) fn interno() { }
    fn privado() { }   // só neste módulo
}

use utils::helper;

// Ficheiros:
// src/utils.rs       -> mod utils
// src/utils/mod.rs   -> alternativa
// src/utils/db.rs    -> mod utils { mod db }

// Re-export:
pub use utils::helper;

Módulos organizam o código e controlam visibilidade. Tudo é privado por defeito — pub expõe, pub(crate) limita ao crate. Módulos mapeiam para ficheiros: mod utils procura src/utils.rs. O use importa caminhos para o scope.

Documentação
/// Soma dois números.
///
/// # Examples
/// ```
/// let r = somar(2, 3);
/// assert_eq!(r, 5);
/// ```
pub fn somar(a: i32, b: i32) -> i32 {
    a + b
}

//! Documentação do módulo/crate
//! (no topo do ficheiro)

// cargo doc --open  (gera HTML)

Doc comments (///) documentam itens e suportam Markdown. Os blocos ``` são testes executáveis (cargo test corre-os). //! documenta o módulo/crate. cargo doc --open gera a documentação HTML (como docs.rs).

Crates essenciais
# Web:
axum / actix-web   # frameworks web
reqwest            # cliente HTTP
serde / serde_json # serialização

# Async e dados:
tokio              # runtime async
sqlx / diesel      # bases de dados
rayon              # paralelismo

# Qualidade:
anyhow / thiserror # erros
clap               # CLI args
tracing            # logging

O ecossistema Rust (crates.io) tem bibliotecas maduras: serde para serialização, tokio para async, reqwest para HTTP, axum/actix-web para servidores. anyhow/thiserror para erros e clap para CLIs.

Visibilidade
pub struct Config {
    pub host: String,    // público
    pub(crate) porta: u16, // só no crate
    token: String,       // privado
}

pub mod api {
    pub(super) fn interno() { } // módulo pai
    pub fn externo() { }
}

// Enum: variantes seguem a visibilidade do enum
pub enum Estado {
    Ativo,    // público
    Inativo,  // público
}

A visibilidade é opt-in: pub (público), pub(crate) (dentro do crate), pub(super) (módulo pai) ou privado (sem modificador). Campos de struct são privados por defeito. Variantes de enum seguem a visibilidade do próprio enum.

Clippy e fmt
# Clippy (linter oficial):
cargo clippy
cargo clippy --fix   # corrige automaticamente

# Exemplo de aviso:
# if x == true  ->  if x
# vec![].len() == 0  ->  is_empty()

# rustfmt (formatação):
cargo fmt            # formata tudo
cargo fmt --check    # só verifica (CI)

# rustfmt.toml (config):
# max_width = 100
# tab_spaces = 4

cargo clippy é o linter oficial com centenas de regras que detetam código não-idiomático e bugs potenciais. O cargo fmt formata automaticamente segundo o estilo padrão da comunidade. Ambos devem correr no CI para consistência.

Macros declarativas
// macro_rules! (pattern matching em tokens):
macro_rules! calcular {
    ($a:expr, +, $b:expr) => { $a + $b };
    ($a:expr, *, $b:expr) => { $a * $b };
}
let r = calcular!(3, +, 4); // 7

// Repetição:
macro_rules! vec_strs {
    ($($x:expr),*) => {
        vec![$($x.to_string()),*]
    };
}
let v = vec_strs!["a", "b", "c"];

macro_rules! faz pattern matching em tokens e expande para código em compilação. Os padrões $x:expr capturam expressões; $($x),* repete zero ou mais vezes. Macros evitam repetição mantendo type-safety (diferente de macros em C).

Atributos
// Condicionais de compilação:
#[cfg(target_os = "windows")]
fn caminho() -> &str { "C:\\" }

#[cfg(feature = "extra")]
fn recurso_extra() { }

// Suprimir avisos:
#[allow(unused_variables)]
let x = 5;

#![allow(dead_code)]  // crate inteiro

// Derive:
#[derive(Debug, Clone)]
struct Ponto { x: f64, y: f64 }

Atributos (#[...]) dão metadados ao compilador. #[cfg] compila condicionalmente (por SO, feature...). #[allow] suprime avisos específicos. #[derive] gera implementações de traits. #![...] aplica-se ao item envolvente (ex: crate).