Closures em Rust: magia ou matemática rigorosa?
\n\nSe você já escreveu algo como |x| x + 1 em Rust, você usou closures. Mas assim que você tenta passar essa closure para uma função ou armazená-la em uma struct, o compilador de repente começa a exigir: “Qual trait? Fn, FnMut ou FnOnce?”.
E é aí que muitos ficam confusos. Especialmente se você vem de JavaScript, Python ou Ruby, onde closures são “apenas funções que capturam variáveis”. Em Rust é mais complexo (e mais interessante).
\n\nHoje vamos entender o que realmente acontece por baixo dos panos quando o Rust decide qual trait dar à sua closure. Não é magia — é um sistema de tipos rigoroso que protege você de condições de corrida e ponteiros soltos. Vamos nessa!
\n\nResumindo: as três traits de closures
\n\nEm Rust, existem três traits que as closures implementam:
\n\n- \n
FnOnce— pode ser chamada uma vez. Assume a propriedade das variáveis capturadas. \n FnMut— pode ser chamada múltiplas vezes, mas pode modificar as variáveis capturadas (usa&mut). \n Fn— pode ser chamada múltiplas vezes e não modifica as variáveis capturadas (usa&). \n
Cada closure implementa automaticamente todas as traits aplicáveis. Ou seja, se uma closure implementa Fn, ela automaticamente implementa também FnMut e FnOnce. Mas o contrário não funciona.
Por quê? Porque Fn é a mais restritiva, e FnOnce é a mais fraca. A hierarquia de herança é assim:
FnOnce ← FnMut ← Fn\n\nOu seja, FnMut: FnOnce, e Fn: FnMut. Lembre-se dessa escada — ela é fundamental.
Como o Rust decide qual trait dar à closure?
\n\nO compilador observa como exatamente a closure usa as variáveis capturadas. Existem três modos de captura:
\n\n- \n
- Por referência (
&T) — a closure apenas lê a variável. \n - Por referência mutável (
&mut T) — a closure modifica a variável. \n - Por valor (
T) — a closure assume a propriedade da variável. \n
Veja como isso afeta as traits:
\n\n| Modo de captura | Traits implementadas | Exemplo |
|---|---|---|
Apenas &T | Fn + FnMut + FnOnce | || println!("{}", x) |
&mut T (mas não T) | FnMut + FnOnce | || x += 1 |
T (propriedade) | Apenas FnOnce | || drop(x) ou move || x |
Vamos analisar cada caso com código real.
\n\nExemplo 1: Apenas leitura → Fn
\n\nlet x = 42;\nlet read_closure = || println!("x = {}", x);\n\n// Pode ser chamada quantas vezes quiser\nread_closure();\nread_closure();\n\n// E até passada para uma função que exige Fn\nfn call_fn<F: Fn()>(f: F) {\n f();\n}\ncall_fn(read_closure);\n\nAqui a closure apenas lê x. Ela não precisa modificá-lo ou assumir sua propriedade. O Rust entende: “Ok, isso é seguro — damos Fn”.
Exemplo 2: Modificação → FnMut
\n\nlet mut x = 42;\nlet mut mut_closure = || {\n x += 1;\n println!("x = {}", x);\n};\n\nmut_closure(); // x = 43\nmut_closure(); // x = 44\n\n// Passamos para uma função que exige FnMut\nfn call_fn_mut<F: FnMut()>(mut f: F) {\n f();\n f();\n}\ncall_fn_mut(mut_closure);\n\nObserve: declaramos mut_closure como let mut. Por quê? Porque chamar FnMut exige mutabilidade