Closures e os Traits Fn do Rust
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.
Resumo
- 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 pormovepode 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.
Fundamentos
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:
struct Closure<'a> { factor: &'a i32 }
impl<'a> Closure<'a> {
fn call(&self, x: i32) -> i32 { x + *self.factor }
}É 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.
Mecânicas e Interações
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.
Considerações Avançadas e Aplicações
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.
Concepções Equivocadas Comuns
- "
Fn,FnMuteFnOncesã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 todaFné automaticamente tambémFnMuteFnOnce. - "
moveeFnOncesignificam a mesma coisa" -movecontrola como as variáveis são capturadas (por valor, no momento da criação da closure); os traitsFndescrevem como a closure pode ser chamada (por ref, mut ref, ou por valor). Uma closuremoveque apenas lê suas capturas ainda éFn, nãoFnOnce. - "O tipo de uma closure é
Fn,FnMut, ouFnOnce" - 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, ouFnOnce" - 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 viamove.
FAQs
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.
Relacionados
- Noções Básicas de Closures - exemplos práticos de sintaxe e captura que se baseiam no modelo mental desta página.
- Fn, FnMut & FnOnce - uma visão lado a lado, orientada a receitas, de como definir parâmetros de closure com cada trait.
- Closures
move- quando e por quemoveé necessário, especialmente para threads e blocos assíncronos. - Funções de Ordem Superior - passando e retornando closures como valores comuns.
- Adaptadores de Iterador - o lugar mais comum onde closures e os traits
Fnse encontram no código do dia a dia.
Versões de Stack: Esta página é conceitual e não está vinculada a uma versão específica do stack.