Uma closure em Rust parece algo pequeno e informal - um |x| x + 1 inserido em uma chamada map - mas por baixo dos panos é uma struct totalmente tipada que o compilador gera para você, com campos para tudo o que ela capturou de seu entorno e um método call para seu corpo. Essa lacuna entre a casualidade com que as closures são lidas e a precisão com que são tipadas é a fonte da maior parte da confusão em torno delas, e é exatamente aí que Fn, FnMut e FnOnce entram.
Esta página é a âncora conceitual para a seção de closures e padrões funcionais. Em vez de percorrer a sintaxe de closures passo a passo - isso é o que Noções Básicas de Closures faz - ela explica o que uma closure realmente é para o compilador, como a captura de ambiente decide qual dos três traits de chamada se aplica, e por que tratar Fn/FnMut/FnOnce como uma hierarquia de propriedade, e não três rótulos não relacionados, é a maneira mais rápida de parar de adivinhar erros de compilação relacionados a closures.
Uma closure é uma struct anônima que contém seu ambiente capturado, mais uma implementação de um de três traits que descrevem como ela pode ser chamada.
Por que Importa: Passar comportamento como um valor - um callback, um comparador, uma transformação - precisa de uma maneira de agrupar estado com código, e Rust faz isso sem um coletor de lixo, tornando esse agrupamento uma struct normal, verificada quanto à propriedade.
Conceitos Chave:captura, ambiente, Fn/FnMut/FnOncehierarquia de traits, move, inferência de tipo de closure.
Quando Usar: Sempre que o comportamento precisar viajar com dados - adaptadores de iterador, callbacks, comparadores de ordenação, trabalho adiado, corpos de thread.
Limitações / Trade-offs: Cada closure tem um tipo distinto, muitas vezes impronunciável, é por isso que armazenar closures heterogêneas requer Box<dyn Fn...>; capturar por move pode forçar clones indesejados quando um empréstimo teria sido suficiente.
Tópicos Relacionados: adaptadores de iterador, genéricos e limites de trait, propriedade e empréstimo, criação de threads.
Quando o compilador vê |x| x + factor, ele faz duas coisas: ele descobre quais variáveis livres o corpo da closure se refere (aqui, factor), e ele gera um tipo de struct único e sem nome com um campo para cada uma delas. Chamar a closure é chamar um método nessa struct - conceitualmente, |x| x + factor compila para algo como:
É por isso que cada closure tem seu próprio tipo, mesmo duas closures com corpos de aparência idêntica. É também por isso que closures podem ser pequenas e rápidas: não há boxing, nenhuma vtable e nenhuma alocação de heap, a menos que você peça explicitamente (por exemplo, com Box<dyn Fn(...)>).
O passo de captura é onde a propriedade entra em cena. Por padrão, o compilador captura cada variável da maneira menos invasiva que o corpo da closure permite: por referência compartilhada se o corpo apenas a lê, por referência mutável se o corpo a muta, e por valor apenas se o corpo precisar possuí-la (por exemplo, movendo-a para um valor retornado, ou enviando-a para outra thread). A palavra-chave move substitui essa inferência e força cada variável capturada a ser pega por valor, independentemente do que o corpo exigiria de outra forma.
Uma vez que o modo de captura de uma closure é decidido, o compilador atribui a ela exatamente um de três traits, escolhido como a interface menos restritiva que o corpo da closure requer:
Fn : chamar quantas vezes quiser, via &self (lê capturas)
FnMut : chamar quantas vezes quiser, via &mut self (lê/escreve capturas)
FnOnce : chamar no máximo uma vez, via self (pode consumir capturas)
Estas não são três categorias disjuntas - elas formam uma hierarquia. Toda closure Fn é também uma FnMut válida (chamá-la através de &mut self é estritamente menos exigente do que chamá-la através de &self), e toda closure FnMut é também uma FnOnce válida. Portanto, FnOnce é o trait que toda closure implementa no mínimo, e Fn é a garantia mais forte: ela pode ser chamada repetidamente através de uma referência compartilhada porque nunca muta ou consome o que capturou. É por isso que APIs genéricas que desejam flexibilidade máxima de seu chamador definem seu parâmetro o mais solto possível - impl FnOnce() para algo chamado exatamente uma vez, impl FnMut() para algo chamado repetidamente com estado interno, impl Fn() apenas quando a closure genuinamente não precisa de mutação.
O trait que o compilador escolhe é inteiramente impulsionado pelo que o corpo da closure faz com suas capturas, não por qualquer coisa que você escreva explicitamente. Uma closure que apenas lê um Vec capturado é Fn. A mesma closure, se você adicionar .push(...) ao seu corpo, torna-se FnMut - a seleção do trait mudou porque o padrão de acesso mudou, mesmo que nada sobre o código circundante tenha mudado. É também por isso que thread::spawn requer FnOnce + Send + 'static: uma closure gerada executa exatamente uma vez em outra thread, suas capturas devem ser totalmente possuídas (sem empréstimos pendentes através da fronteira da thread), e move é quase sempre necessário para satisfazer isso.
// Mesma forma, trait diferente, por causa do que o corpo faz:let log = vec![String::from("start")];let read_only = || println!("{log:?}"); // Fn: apenas lê `log`let mut counter = 0;let mutate = || { counter += 1; }; // FnMut: escreve em `counter`let consume = move || drop(log); // FnOnce: toma posse de `log`
Cada uma dessas três closures tem um tipo diferente e incompatível do ponto de vista do compilador, mesmo que todas "pareçam closures". Uma função que deseja aceitar qualquer uma delas genericamente escreve fn run(f: impl FnOnce()), o limite mais solto que cobre todas as três, já que toda closure Fn e FnMut é também uma FnOnce válida.
Como cada closure tem um tipo distinto e gerado pelo compilador, armazenar várias closures diferentes em um só lugar - uma lista de callbacks, uma tabela de plugins - não pode usar um Vec<SomeClosureType> simples. As duas escapatórias padrão são genéricos (quando o conjunto de closures é conhecido em tempo de compilação e a monomorfização é aceitável) e objetos de trait (Box<dyn Fn(...)> ou Box<dyn FnMut(...)>, quando o conjunto é dinâmico ou deve ser armazenado uniformemente). Objetos de trait adicionam uma indireção e uma chamada de vtable; genéricos evitam esse custo, mas produzem uma cópia especializada do código de chamada por tipo de closure distinto, trocando o tamanho do binário por velocidade.
Abordagem
Força
Fraqueza
Melhor Encaixe
impl Fn/FnMut/FnOnce genérico
Custo zero, inlines completamente, sem alocação
Uma cópia monomorfizada por tipo de closure, não pode armazenar closures mistas em um Vec
Caminhos quentes (hot paths), adaptadores de iterador, APIs de closure única
Box<dyn Fn(...)> / Box<dyn FnMut(...)>
Tipo uniforme, armazenável em coleções, despacho dinâmico
Alocação de heap, sobrecarga de chamada de vtable, perde alguma inlining
Registros de callback, APIs estilo plugin, listas de closures heterogêneas
Ponteiro fn simples
O menor, sem captura alguma
Não pode capturar nenhum ambiente
Callbacks sem estado, ponteiros de função compatíveis com C
Closures FnMut armazenadas entre chamadas são uma fonte comum de bugs sutis: um Box<dyn FnMut()> retido em um campo de struct significa que cada chamada pode mutar o estado capturado da closure, então dois proprietários da mesma closure em caixa (o que não é possível sem Rc<RefCell<...>> ou similar) precisariam do mesmo tratamento de mutabilidade interna que qualquer outro estado mutável compartilhado. Esta é uma razão pela qual closures e ponteiros inteligentes aparecem juntos com tanta frequência - um padrão Rc<RefCell<FnMut closure>> é apenas o equivalente de closure de qualquer outro problema de estado mutável compartilhado.
APIs de ordenação são uma boa ilustração da mordida prática da hierarquia: slice::sort_by aceita FnMut(&T, &T) -> Ordering porque um comparador pode ser chamado muitas vezes e pode razoavelmente carregar contadores ou caches, mas nunca precisa ser chamado através de &mut self de uma forma que consuma algo, então FnOnce seria um limite fraco demais (não permitiria chamadas repetidas) e Fn seria um requisito desnecessariamente forte para o chamador.
"Fn, FnMut e FnOnce são três tipos separados de closure" - São níveis de capacidade aninhados na mesma closure, não uma divisão em três vias; o compilador escolhe o mais forte que o corpo da closure satisfaz de fato, e toda Fn é automaticamente também FnMut e FnOnce.
"move e FnOnce significam a mesma coisa" - move controla como as variáveis são capturadas (por valor, no momento da criação da closure); os traits Fn descrevem como a closure pode ser chamada (por ref, mut ref, ou por valor). Uma closure move que apenas lê suas capturas ainda é Fn, não FnOnce.
"O tipo de uma closure é Fn, FnMut, ou FnOnce" - Esses são traits que o tipo único e gerado pelo compilador da closure implementa; o tipo concreto em si é anônimo e efetivamente impronunciável fora do compilador.
"Closures sempre alocam no heap" - Uma closure é uma struct comum alocada na pilha como qualquer outra, a menos que você a coloque explicitamente em uma caixa (Box<dyn Fn...>) para apagar seu tipo concreto para armazenamento ou despacho dinâmico.
"Você escolhe se uma closure é Fn, FnMut, ou FnOnce" - Você não a declara; o compilador a infere estritamente do que o corpo da closure faz com suas capturas. A única alavanca que você controla diretamente é o modo de captura via move.
Estruturalmente, o que é uma closure para o compilador Rust?
Uma struct anônima com um campo por variável capturada, mais um método gerado que implementa a chamada. Duas closures com corpos de aparência idêntica ainda têm tipos distintos, pois cada uma é compilada independentemente.
Como o Rust decide se captura uma variável por referência ou por valor?
Inspecionando o que o corpo da closure faz com ela: acesso somente leitura captura por referência compartilhada, mutação captura por referência mutável, e apenas uso que requer propriedade (ou um move explícito) captura por valor. O compilador escolhe o modo menos invasivo que o corpo realmente precisa.
O que `move` realmente muda?
Força cada variável capturada a ser pega por valor em vez de por referência, independentemente do que o corpo da closure estritamente requer. Não determina, por si só, se a closure é Fn, FnMut, ou FnOnce - uma closure move que apenas lê suas capturas possuídas ainda pode ser Fn.
Por que `Fn`, `FnMut` e `FnOnce` são descritos como uma hierarquia em vez de três traits separados?
Porque Fn: FnMut e FnMut: FnOnce - toda closure Fn é automaticamente uma FnMut válida, e toda FnMut é automaticamente uma FnOnce válida. Isso permite que o código genérico defina seu parâmetro tão solto quanto precisa (FnOnce para "chamável uma vez", Fn para "chamável repetidamente sem mutação") e aceite qualquer closure que satisfaça pelo menos esse nível.
Por que `thread::spawn` requer `FnOnce + Send + 'static`?
A closure gerada executa exatamente uma vez em outra thread (FnOnce), seus dados capturados devem ser transferíveis com segurança através da fronteira da thread (Send), e ela não pode emprestar nada que possa não sobreviver à thread ('static). move é tipicamente necessário para satisfazer a parte de propriedade disso, já que um empréstimo simples poderia ficar pendente assim que a função de geração retornasse.
Uma closure pode ter seu trait inferido alterado se eu editar seu corpo sem tocar em sua assinatura?
Sim - adicionar uma mutação (como .push(...)) a uma closure que anteriormente apenas lia suas capturas eleva seu trait requerido de Fn para FnMut, puramente por causa do que o corpo agora faz. Nada sobre como a closure é declarada precisa mudar para que isso aconteça.
Por que não posso colocar várias closures diferentes em um `Vec`?
Porque cada closure tem seu próprio tipo distinto e gerado pelo compilador, mesmo que suas assinaturas correspondam. Um Vec<T> precisa de um T concreto, então closures mistas precisam ser unificadas através de genéricos (se o conjunto for fixo e conhecido) ou através de um objeto de trait como Box<dyn Fn(...)>, que apaga o tipo concreto por trás de uma vtable.
Qual é a diferença prática de custo entre um parâmetro de closure genérico e `Box`?
Um parâmetro genérico impl Fn(...) é monomorfizado - o compilador gera uma cópia especializada do código de chamada para cada tipo de closure distinto, que geralmente é totalmente inlined e não custa nada extra em tempo de execução, ao custo de binários maiores. Box<dyn Fn(...)> apaga o tipo por trás de uma vtable compartilhada, custando uma alocação de heap e uma chamada indireta, mas permitindo o armazenamento uniforme de muitas closures diferentes.
Por que `slice::sort_by` aceita `FnMut` em vez de `Fn` ou `FnOnce`?
Um comparador pode ser invocado muitas vezes durante uma ordenação (excluindo FnOnce), mas não há razão para proibi-lo de carregar estado interno mutável como um contador ou cache (o que Fn proibiria), então FnMut é exatamente o nível certo de restrição - chamável repetidamente, permitido mutar suas capturas.
Chamar uma closure `FnOnce` duas vezes é um erro em tempo de execução ou um erro de compilação?
Um erro de compilação. O método FnOnce::call_once pega self por valor, então após a primeira chamada o valor da closure foi consumido; o compilador rejeita estaticamente qualquer caminho de código que tente chamá-la novamente, da mesma forma que rejeita o uso de qualquer outro valor movido.
Ponteiros de função (`fn(...)`) se relacionam com os traits `Fn`?
Sim - um ponteiro fn simples (que não captura nada) implementa Fn, FnMut e FnOnce simultaneamente, pois satisfaz todos os três modos de chamada trivialmente. É o caso especial de uma "closure" com um ambiente vazio, que também é por que o código genérico limitado por traits Fn aceita funções comuns, não apenas closures com sintaxe |...|.
Por que armazenar uma closure `FnMut` em uma struct às vezes precisa de `RefCell` ou similar?
Chamar uma closure FnMut requer uma referência mutável a ela (&mut self), então se a closure for alcançável através de uma referência compartilhada ao seu proprietário (por exemplo, por trás de um Rc), você precisa de mutabilidade interior para obter esse acesso &mut - o mesmo requisito que se aplica à mutação de qualquer outro campo alcançável apenas através de &self.