Closures em Rust: captura por referência e por valor

1. Introdução às Closures em Rust

Closures em Rust são funções anônimas que podem capturar variáveis do escopo onde são definidas. Diferentemente de funções tradicionais, closures têm acesso ao ambiente ao seu redor, permitindo comportamentos dinâmicos e flexíveis.

A sintaxe básica de uma closure é simples: |parâmetros| expressão. Por exemplo:

let soma = |a: i32, b: i32| a + b;
println!("{}", soma(3, 4)); // 7

Rust infere os tipos dos parâmetros e do retorno na maioria dos casos, mas você pode anotá-los explicitamente:

let soma_anotada = |a: i32, b: i32| -> i32 { a + b };

A principal diferença entre closures e funções tradicionais é a capacidade de capturar variáveis do ambiente:

let fator = 2;
let multiplicar = |x: i32| x * fator; // captura 'fator' do ambiente
println!("{}", multiplicar(5)); // 10

2. Mecanismos de Captura: Referência Imutável, Referência Mutável e Valor

Rust oferece três modos de captura, determinados automaticamente pelo compilador baseado no uso da variável dentro da closure:

Captura por referência imutável (&T)

Quando a closure apenas lê a variável, Rust a captura como referência imutável:

let mensagem = String::from("Olá");
let imprimir = || println!("{}", mensagem); // captura &String
imprimir();
println!("Ainda posso usar: {}", mensagem); // funciona!

Captura por referência mutável (&mut T)

Quando a closure modifica a variável, Rust a captura como referência mutável:

let mut contador = 0;
let mut incrementar = || {
    contador += 1; // captura &mut i32
    println!("Contador: {}", contador);
};
incrementar(); // Contador: 1
incrementar(); // Contador: 2
// println!("{}", contador); // erro! emprestado mutavelmente

Captura por valor (T)

Quando a closure precisa mover a variável para seu interior (ex: para enviar a outra thread):

let nome = String::from("Alice");
let saudacao = || {
    let _ = nome; // captura por valor, move ownership
    println!("Tchau!");
};
saudacao();
// println!("{}", nome); // erro! nome foi movido

3. O Trait Fn, FnMut e FnOnce: Como o Compilador Escolhe

Rust classifica closures em três traits baseados no modo de captura:

Fn — Captura por referência imutável

Closures que não modificam o ambiente e podem ser chamadas múltiplas vezes sem efeitos colaterais:

fn executar_fn<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(x) + f(x) // pode chamar várias vezes
}

let dobro = |x| x * 2;
println!("{}", executar_fn(dobro, 5)); // 20

FnMut — Captura por referência mutável

Closures que modificam o ambiente e podem ser chamadas múltiplas vezes:

fn executar_fnmut<F: FnMut(i32) -> i32>(mut f: F, x: i32) -> i32 {
    f(x) + f(x) // precisa de mut
}

let mut acumulador = 10;
let mut soma = |x| {
    acumulador += x;
    acumulador
};
println!("{}", executar_fnmut(soma, 5)); // 20 (10+5 + 15+5)

FnOnce — Captura por valor

Closures que consomem o ambiente e podem ser chamadas apenas uma vez:

fn executar_fnonce<F: FnOnce(String) -> String>(f: F, s: String) -> String {
    f(s) // só pode chamar uma vez
}

let prefixo = String::from("Sr. ");
let saudacao = |nome| format!("{}{}", prefixo, nome);
println!("{}", executar_fnonce(saudacao, "João".to_string())); // Sr. João

O compilador infere automaticamente o trait menos restritivo possível. Uma closure Fn também implementa FnMut e FnOnce, uma FnMut também implementa FnOnce, mas não Fn.

4. Captura por Referência: Implicações e Boas Práticas

Capturar por referência é eficiente e não transfere ownership, mas requer cuidado com lifetimes e regras de borrowing.

Exemplo com iteradores e filter:

let numeros = vec![1, 2, 3, 4, 5, 6];
let limite = 3;

let maiores: Vec<&i32> = numeros.iter()
    .filter(|&&x| x > limite) // captura &limite por referência imutável
    .collect();

println!("{:?}", maiores); // [4, 5, 6]

Boas práticas:
- Prefira captura por referência quando possível para evitar cópias desnecessárias
- Lembre-se que múltiplas closures podem compartilhar referências imutáveis simultaneamente
- Apenas uma closure pode ter referência mutável por vez

5. Captura por Valor: Movendo Ownership para a Closure

Use a palavra-chave move para forçar captura por valor, essencial quando a closure precisa sobreviver ao escopo original:

use std::thread;

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

thread::spawn(move || {
    println!("Dados na thread: {:?}", dados);
    // dados foi movido para a closure
}).join().unwrap();

// println!("{:?}", dados); // erro! dados foi movido

Cenários comuns para move:
- thread::spawn — a closure precisa ter ownership dos dados
- async tasks — dados precisam sobreviver a pontos de suspensão
- Retorno de closures de funções — a closure precisa ser independente

6. Closures como Argumentos de Função: Genéricos e Traits

Funções podem aceitar closures usando genéricos com impl Fn ou Box<dyn Fn>:

fn aplicar_tres_vezes<F>(mut f: F, x: i32) -> i32
where
    F: FnMut(i32) -> i32,
{
    f(f(f(x)))
}

let mut multiplicador = 2;
let mut multiplicar = |x| x * multiplicador;
println!("{}", aplicar_tres_vezes(multiplicar, 1)); // 8 (1*2*2*2)

impl Trait vs dyn Trait:
- impl Fn — monomorfização, melhor performance, tipos concretos
- Box<dyn Fn> — dispatch dinâmico, útil para coleções heterogêneas

fn coletar_closures() -> Vec<Box<dyn Fn(i32) -> i32>> {
    vec![
        Box::new(|x| x + 1),
        Box::new(|x| x * 2),
        Box::new(|x| x - 3),
    ]
}

let closures = coletar_closures();
for f in closures {
    println!("{}", f(10));
}

7. Closures Retornadas de Funções e Lifetimes

Retornar closures que capturam referências requer anotações de lifetime:

fn criar_saudacao<'a>(nome: &'a str) -> impl Fn() -> String + 'a {
    move || format!("Olá, {}!", nome)
}

let nome = String::from("Maria");
let saudacao = criar_saudacao(&nome);
println!("{}", saudacao()); // Olá, Maria!

Sem a anotação de lifetime, o compilador não pode garantir que a referência ainda será válida quando a closure for chamada. O uso de move força a captura por valor, resolvendo problemas de dangling references.

8. Casos Avançados e Erros Comuns

Captura mista de múltiplas variáveis

let mut a = 5;
let b = 10;
let closure = || {
    a += 1; // captura &mut a
    println!("{}", b); // captura &b
};
closure();
// println!("{}", a); // erro! emprestado mutavelmente

Erro de borrowing conflitante

let mut lista = vec![1, 2, 3];
let ref1 = &lista;
let closure = || {
    lista.push(4); // erro! já emprestado imutavelmente
};
println!("{:?}", ref1);

Closures aninhadas

let externo = String::from("externa");
let interna = {
    let mut contador = 0;
    move || {
        contador += 1;
        format!("{} {}", externo, contador)
    }
};
println!("{}", interna()); // externa 1
println!("{}", interna()); // externa 2

Dicas de depuração: Quando encontrar erros com closures, leia atentamente as mensagens do compilador. Elas geralmente indicam qual variável foi movida ou emprestada, e em qual closure ocorreu o conflito. Use --explain para obter explicações detalhadas.

Referências