CodeGym /Cursos /C# SELF /Closures

Closures

C# SELF
Nível 51 , Lição 3
Disponível

1. Introdução

Em programação closure — não é uma maneira de fechar a porta no JavaScript, e sim um mecanismo pelo qual uma expressão lambda ou um método anônimo captura variáveis do contexto ao redor e "lembra" delas mesmo depois que o bloco onde foram declaradas terminou. Simplificando, um closure é uma função que lembrou as condições em que nasceu, e guarda esses valores como uma pequena malinha com pertences pessoais (variáveis).

Closure — é uma função junto com o ambiente (scope) que existia no momento da sua criação.

Exemplo mais simples de closure

Vamos ver na prática:


Func<int> MakeCounter()
{
    int count = 0;
    return () =>
    {
        count++;
        return count;
    };
}

Chamando assim:


var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3

Como isso funciona?

  • A variável count é declarada dentro do método MakeCounter.
  • A lambda () => { ... } é retornada para fora e agora vive fora do método.
  • Mas! Ela lembra a variável count, mesmo que o próprio método MakeCounter já tenha terminado.

Isso é um closure: a lambda "capturou" a variável count do contexto ao redor.

O que exatamente a lambda "captura"?

  • variáveis locais do método envolvente (scope),
  • parâmetros do método,
  • variáveis em blocos (for, foreach, etc.).

Importante: as variáveis são capturadas não por valor, mas por referência! Se mudarmos a variável dentro do closure, ela mudará "por fora" também. Na prática o compilador C# cria uma classe auxiliar para essas variáveis — mas é suficiente lembrar conceitualmente disso para usar closures com confiança.

2. Closure e escopo léxico

Vamos melhorar o exemplo da "fábrica de funções" com closure:


Func<int, int> PowerFactory(int power)
{
    return x =>
    {
        int result = 1;
        for (int i = 0; i < power; i++)
            result *= x;
        return result;
    };
}

Uso:


var square = PowerFactory(2);   // x^2
var cube = PowerFactory(3);     // x^3

Console.WriteLine(square(5)); // 25
Console.WriteLine(cube(2));   // 8

As funções square e cube foram criadas com valores diferentes da variável power, e cada uma lembra o seu. Para cada chamada de PowerFactory é criado uma "mochila" própria com os valores capturados.

3. Mutação de variáveis capturadas

Às vezes surge a pergunta: o que acontece se num loop criarmos várias lambdas que capturam a variável do loop? Aí é fácil pisar na armadilha.

Exemplo: closure no loop


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
    action(); // ???

O que você espera? 0, 1, 2? Na prática será:

3
3
3

Por que isso acontece? Todas as lambdas "referenciam" a mesma variável i. No fim do loop i já vale 3, e é esse valor que todas as nossas Action vão ver.

Versão corrigida

Para que cada lambda capture seu próprio valor, crie uma nova variável dentro do corpo do loop:


var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    int copy = i;
    actions.Add(() => Console.WriteLine(copy));
}
foreach (var action in actions)
    action(); // 0 1 2

Agora copy é uma nova variável em cada iteração, e o closure captura exatamente ela.

4. Aplicações de closures em problemas reais

Processamento de dados e callbacks

Quando você faz algo assíncrono ou adia a execução (handlers de eventos, filtragem, agendamento de tarefas) — closure permite "embalar" a lógica junto com os parâmetros. Por exemplo:


void ProcessList(List<int> list, int threshold)
{
    var filtered = list.Where(x => x > threshold);
    foreach (var item in filtered)
        Console.WriteLine(item);
}

Aqui a lambda dentro de Where captura a variável threshold.

Criando "fábricas" de funções

Você passa um parâmetro — recebe uma função com esse parâmetro "embutido". Esse padrão é útil pra configurar filtros, comparadores de ordenação, reações de UI etc.

Gerenciamento de estado

Às vezes é preciso manter um pouco de estado sem criar uma classe separada:


Func<string, string> CreateGreeting()
{
    string prefix = "Hello";
    return name =>
    {
        return $"{prefix}, {name}!";
    };
}

5. Nuances úteis

Debaixo do capô: como closure funciona em C#

Tudo que é capturado pela lambda o compilador transforma em uma classe auxiliar: variáveis viram campos, e a lambda vira um método. Por isso o estado das variáveis "vive" entre chamadas.

Cada "fábrica" gera um pequeno objeto. Isso é normal — o .NET gerencia esses objetos de forma eficiente e os aloca só quando realmente necessário.

Lembrete: não capture objetos "grandes" sem necessidade

Se você capturar um objeto grande (por exemplo, uma UI form), ele não será liberado enquanto a lambda estiver viva. A razão clássica de vazamento é assinar eventos com uma lambda que capturou um contexto "pesado" e não desassinar (+=/-=).

Closures e vida de coleções — exemplo com LINQ

Closures tornam o LINQ flexível: filtros lembram seus parâmetros.


List<string> colors = new List<string> { "Red", "Green", "Blue", "Yellow" };
string startsWith = "B";
var filtered = colors.Where(c => c.StartsWith(startsWith));
foreach (var color in filtered)
    Console.WriteLine(color); // "Blue"

Se depois você mudar startsWith, o resultado também muda:


startsWith = "R";
foreach (var color in filtered)
    Console.WriteLine(color); // "Red"

Isso acontece porque o closure referencia a mesma variável startsWith, e o método StartsWith checa o valor atual cada vez.

6. Erros típicos ao trabalhar com closures

Erro nº1: capturar uma variável que muda antes do uso.
Situação clássica em loops: a lambda dentro do closure "olha" para a mesma variável do loop, que no momento da chamada já tem outro valor. No fim a função trabalha com dados diferentes dos esperados. Resolve-se introduzindo uma variável separada dentro do loop.

Erro nº2: capturar contexto grande demais.
O closure carrega o objeto inteiro em vez de um campo/valor específico. Isso cria dependências desnecessárias e deixa o código mais complexo. Capture só o que precisa.

Erro nº3: manter recursos pesados e vazamento de memória.
Assinar um evento com uma lambda que capturou um objeto "pesado" e não desassinar — o objeto não é liberado. Fique de olho no tempo de vida das assinaturas e use desinscrição explícita (-=).

Erro nº4: perda de gerenciabilidade do código.
Uso excessivo de closures torna difícil entender de onde vêm os dados e quando eles mudam, especialmente quando o closure é declarado longe do ponto de chamada. Mantenha a lógica próxima e não abuse.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION