Introdução: por que callbacks em Rust são um desafio (e uma oportunidade)
\nSe você já escreveu em JavaScript, Python ou C#, os callbacks são uma ferramenta familiar para você. Você passa uma função que será chamada mais tarde e vive tranquilo. Mas quando você chega ao Rust, esse padrão simples de repente se transforma em um campo minado: propriedade, empréstimo, tempos de vida, dyn vs impl...
No Stack Overflow, a pergunta "Idiomatic callbacks in Rust" acumulou milhares de visualizações. Isso significa que até desenvolvedores experientes tropeçam neste tópico. Mas a boa notícia: o Rust oferece não uma, mas várias maneiras elegantes de implementar callbacks. E hoje vamos analisar todas elas — desde closures simples até poderosos trait objects e padrões enum.
\nEste artigo é para você, se você:
\n- \n
- Começou a estudar Rust e quer entender como organizar corretamente arquiteturas assíncronas e orientadas a eventos; \n
- Está migrando de outras linguagens e procurando análogos idiomáticos para padrões familiares; \n
- Quer escrever código Rust limpo, seguro e eficiente. \n
Vamos nessa!
\n\nO problema: o que há de errado com callbacks simples?
\nImagine que estamos escrevendo um agendador de tarefas simples. Em Python, seria assim:
\ndef schedule(callback):\n # faz algo\n callback(result)\nEm Rust, a primeira tentativa pode ser:
\nfn schedule(callback: fn(i32) -> i32) {\n let result = 42;\n callback(result);\n}\nE isso funciona! Mas apenas para funções, não para closures. Tente passar uma closure que captura uma variável:
\nlet factor = 2;\nlet closure = |x| x * factor;\nschedule(closure); // Erro! expected fn pointer, found closure\nO compilador reclama com razão: fn é um ponteiro para função, enquanto uma closure é um tipo anônimo que pode capturar o ambiente. Precisamos de uma abordagem mais flexível.
Abordagem 1: Closures com impl Fn (simplicidade e flexibilidade)
\nA maneira mais simples e frequentemente usada é usar um tipo genérico com o trait Fn, FnMut ou FnOnce.
Quando usar Fn, FnMut, FnOnce?
\n- \n
FnOnce— se o callback for chamado exatamente uma vez (consome os valores capturados); \n FnMut— se o callback puder modificar o estado capturado (chamado várias vezes); \n Fn— se o callback não modificar o estado (função pura). \n
Exemplo com impl Fn:
fn schedule<F>(callback: F)\nwhere\n F: Fn(i32) -> i32,\n{\n let result = 42;\n println!("Resultado: {}", callback(result));\n}\n\nfn main() {\n let factor = 2;\n let closure = |x| x * factor;\n schedule(closure); // Funciona!\n \n // Também pode passar uma função comum\n fn double(x: i32) -> i32 { x * 2 }\n schedule(double);\n}\nVantagens: extremamente flexível, suporta closures, seguro, legível.
\nDesvantagens: monomorfização — para cada tipo de closure, o compilador gera uma versão separada da função. Se houver muitos callbacks, o código pode inchar.
Abordagem 2: Trait objects Box<dyn Fn> (despacho dinâmico)
\nSe você quiser armazenar callbacks em coleções (por exemplo, em um Vec) ou passá-los entre threads, os genéricos não funcionarão — é necessário um único tipo. Os trait objects vêm em socorro:
type Callback = Box<dyn Fn(i32) -> i32>;\n\nstruct Scheduler {\n callbacks: Vec<Callback>,\n}\n\nimpl Scheduler {\n fn new() -> Self {\n Scheduler { callbacks: Vec::new() }\n }\n \n fn register(&mut self, callback