1. Introducción
En programación, un closure —no es una forma de cerrar la puerta en JavaScript— sino un mecanismo por el cual una expresión lambda o un método anónimo captura variables del contexto circundante y las «recuerda» incluso después de que haya terminado el bloque donde se declararon. Más sencillo: un closure es una función que memorizó las condiciones en las que nació y guarda esos valores como una pequeña maleta con objetos personales (variables).
Closure —es una función junto con el entorno (scope) que existía en el momento de su creación.
Ejemplo más simple de closure
Vamos a verlo en práctica:
Func<int> MakeCounter()
{
int count = 0;
return () =>
{
count++;
return count;
};
}
Lo llamamos así:
var counter = MakeCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Console.WriteLine(counter()); // 3
¿Cómo funciona esto?
- La variable count está declarada dentro del método MakeCounter.
- La lambda () => { ... } se devuelve hacia fuera y ahora vive fuera del método.
- ¡Pero! Ella recuerda la variable count, aunque el propio método MakeCounter ya haya terminado.
Esto es un closure: la lambda «capturó» (capturó por referencia) la variable count del contexto circundante.
¿Qué exactamente «captura» la lambda?
- variables locales del método contenedor (scope),
- parámetros del método,
- variables en bloques (for, foreach, etc.).
Importante: las variables se capturan no por valor, sino por referencia. Si cambiamos la variable dentro del closure, cambiará también «afuera». De hecho el compilador de C# crea una clase auxiliar especial para estas variables —pero con esto basta conceptualmente para usar closures con confianza.
2. Closure y ámbito léxico
Mejoramos el ejemplo de «fábrica de funciones» con 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
Las funciones square y cube se crearon con distintos valores de la variable power, y cada una recuerda la suya. Para cada llamada a PowerFactory se crea una «mochila» propia de valores capturados.
3. Mutación de variables capturadas
A veces surge la pregunta: ¿qué pasa si en un bucle creamos varias lambdas que capturan la variable del bucle? Aquí es fácil tropezar.
Ejemplo: closure en un bucle
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions)
action(); // ???
¿Qué esperas? 0, 1, 2? En realidad será:
3
3
3
¿Por qué? Todas las lambdas «refieren» a la misma variable i. Al final del bucle i ya vale 3, y ese es el valor que verán todas nuestras Action.
Versión corregida
Para que cada lambda capture su propio valor, creamos una nueva variable dentro del cuerpo del bucle:
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
Ahora copy es una variable nueva en cada iteración, y el closure captura precisamente esa.
4. Aplicación de closures en tareas reales
Procesamiento de datos y callbacks
Cuando haces algo asincrónico o retrasas ejecución (manejadores de eventos, filtrado, scheduling) —el closure permite «empaquetar» la lógica con parámetros. Por ejemplo:
void ProcessList(List<int> list, int threshold)
{
var filtered = list.Where(x => x > threshold);
foreach (var item in filtered)
Console.WriteLine(item);
}
Aquí la lambda dentro de Where captura la variable threshold.
Crear «fábricas» de funciones
Pasas un parámetro —obtienes una función con ese parámetro «incrustado». Este patrón es útil para configurar filtros, comparadores de ordenación, reacciones de UI, etc.
Gestión de estado
A veces necesitas mantener un poco de estado sin una clase separada:
Func<string, string> CreateGreeting()
{
string prefix = "Hello";
return name =>
{
return $"{prefix}, {name}!";
};
}
5. Matices útiles
Bajo el capó: cómo funciona el closure en C#
Todo lo que la lambda captura, el compilador lo convierte en una clase auxiliar: las variables se vuelven campos, y la lambda —un método. Por eso el estado de las variables «vive» entre llamadas.
Cada «fábrica» genera un pequeño objeto. Eso está bien —.NET maneja eficientemente esos objetos y los asigna sólo cuando es realmente necesario.
Recordatorio: no captures objetos «grandes» sin necesidad
Si capturas un objeto grande (por ejemplo, una forma UI), no se liberará mientras viva la lambda. La causa clásica de fugas es suscribirse a eventos con una lambda que capturó un contexto «pesado» y no desuscribirse (+=/-=).
Closures y la vida de las colecciones — ejemplo con LINQ
Los closures hacen LINQ flexible: los filtros recuerdan sus 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"
Si luego cambias startsWith, cambiará también el resultado:
startsWith = "R";
foreach (var color in filtered)
Console.WriteLine(color); // "Red"
Eso pasa porque el closure referencia la misma variable startsWith, y el método StartsWith comprueba en cada ocasión el valor actual.
6. Errores típicos al trabajar con closures
Error nº1: capturar una variable que cambia antes de usarla.
Situación clásica en bucles: la lambda dentro del closure «mira» la misma variable del bucle, que para el momento de la llamada ya tiene otro valor. Al final la función no trabaja con los datos esperados. Se soluciona declarando una variable separada dentro del bucle.
Error nº2: capturar un contexto demasiado grande.
El closure arrastra todo el objeto en vez de un campo/valor concreto. Eso crea dependencias innecesarias y complica el código. Captura solo lo necesario.
Error nº3: retener recursos pesados y fuga de memoria.
Suscribirse a un evento con una lambda que cerró sobre un objeto «pesado» y no desuscribirse —el objeto no se libera. Vigila el tiempo de vida de las suscripciones y usa desuscripción explícita (-=).
Error nº4: pérdida de controlabilidad del código.
El uso excesivo de closures dificulta entender de dónde vienen los datos y cómo cambian, especialmente cuando el closure se declara lejos del lugar de invocación. Mantén la lógica cerca y no abuses.
GO TO FULL VERSION