CodeGym /Cursos /C# SELF /Captura de variables ( Closures)

Captura de variables ( Closures)

C# SELF
Nivel 49, Lección 4
Disponible

1. ¿Qué es un cierre (closure)?

En programación, un cierre (closure) es una función que captura variables del contexto externo. En pocas palabras, si una expresión lambda o un método anónimo usa variables declaradas fuera de su cuerpo, esa función se convierte en un cierre. "Recuerda" qué valores tenían esas variables en el momento de su creación.

Analogía de la vida real:
Imagina que anotaste una receta secreta en una hoja de papel y la guardaste en un sobre. Incluso si la hoja luego se pierde o deja de ser accesible directamente (la variable deja de estar disponible), quien tenga ese sobre (la lambda) todavía tiene acceso a la receta.

Ejemplo simple:

int x = 42;
Func<int> getX = () => x;
Console.WriteLine(getX()); // 42

Aquí getX es un cierre, porque usa la variable x declarada fuera de sí.

2. ¿Por qué es importante la captura de variables?

En C# los closures se usan prácticamente en todas partes:

  • En colecciones y LINQ queries
  • Para pasar parámetros a eventos o métodos asíncronos
  • Al crear manejadores de eventos dentro de bucles
  • Para almacenar "contexto" entre diferentes llamadas

Sin closures muchas prácticas estándar de C# serían imposibles o muy incómodas.

Ejemplo práctico

Imaginemos que desarrollamos una app de recordatorios: el usuario programa una serie de recordatorios y en algún momento (en un minuto, una hora, una semana...) debe mostrarse el mensaje correcto. Es fácil pasar al manejador una lambda que "recordó" qué mostrar. Eso es captura de variables, clásico.

3. ¿Cómo implementa C# la captura de variables?

Detrás de escena C# hace un truco: cuando tienes una lambda que usa variables externas, el compilador crea automáticamente una clase auxiliar — display class. Todas las variables "capturadas" se convierten en campos de esa clase.

Esquema:


Variable externa ──► DisplayClass
                         ▲
                         │
                    Cierre (lambda)

Ilustración en código

Esto es lo que sucede "bajo el capó":

int x = 5;
Func<int> f = () => x;
// Aquí el compilador hace algo así:
class DisplayClass
{
    public int x;
    public int Lambda() => x;
}
DisplayClass display = new DisplayClass();
display.x = 5;
Func<int> f = display.Lambda;

Esto explica por qué el cierre sigue viendo el valor actual de la variable incluso después de salir del bloque donde fue declarada.

4. ¿El valor de la variable se "congela" o cambia?

En C# las variables se capturan por referencia, no por valor. Eso significa que si una lambda usa una variable y esa variable se modifica en otro lugar, la lambda verá el nuevo valor.

Ejemplo:

int x = 10;
Func<int> getX = () => x;

x = 20;
Console.WriteLine(getX()); // 20, ¡no 10!

Los estudiantes a menudo esperan que getX() siempre devuelva 10, porque la variable fue "capturada". Sin embargo, la lambda lee la variable que todavía existe y que puede cambiar.

¿Cuándo se fija el valor?

Si la variable se declara en un bucle con un nuevo alcance por iteración, por ejemplo usando foreach, y en cada iteración se crea una nueva variable — la lambda "recordará" el valor actual.

5. Ejemplos: captura en bucle — trampa típica

Error común

Queremos crear un array de delegates, cada uno mostrando su propio número del bucle:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    actions[i] = () => Console.WriteLine(i);
}
foreach (var action in actions)
    action();

¿Qué imprimirá el programa?

5
5
5
5
5

¡Vaya! ¿Por qué no 0,1,2,3,4?

Razón:
La lambda captura la misma variable i, que sigue cambiando durante el bucle. Cuando luego invocas los delegates, i ya vale 5.

¿Cómo hacerlo bien?

Hay que crear una variable separada dentro del cuerpo del bucle:

Action[] actions = new Action[5];
for (int i = 0; i < 5; i++)
{
    int index = i; // ¡Nueva variable para cada iteración!
    actions[i] = () => Console.WriteLine(index);
}
foreach (var action in actions)
    action();

Ahora el programa imprimirá:

0
1
2
3
4

¿Cómo se relaciona esto con display class?

En la primera versión todos los delegates "enganchan" el mismo campo — por eso el resultado es igual para todos. En la segunda versión se crea una nueva variable local por cada iteración, así que para cada delegate se forma un DisplayClass separado con un valor único.

6. Escenarios prácticos de uso de captura de variables

Ejemplo 1: manejo de eventos con "contexto"

Supongamos que en nuestra pequeña app hay una lista de tareas, y a cada una está asociado un manejador para el botón "completar". Necesitamos que la lambda dentro del manejador "recorde" qué tarea procesar:

foreach (var task in tasks)
{
    button.Click += (sender, e) => CompleteTask(task);
}

Aquí la variable task se captura en cada iteración. Es importante asegurarse de que está declarada correctamente dentro del bucle para no caer en la trampa del ejemplo anterior.

Ejemplo 2: operaciones asíncronas

A menudo los closures se usan para pasar parámetros a la lógica asíncrona — por ejemplo, guardar una variable en un "slot" local al lanzar una tarea asíncrona:

for (int i = 0; i < 3; i++)
{
    int index = i; // ¡Imprescindible!
    Task.Run(() => Console.WriteLine($"Task #{index}"));
}

Sin la variable local, todas las tareas imprimirían el mismo número, que normalmente no es lo que queríamos.

Ejemplo 3: consultas LINQ

LINQ sobre colecciones usa a menudo closures para filtrar o transformar elementos teniendo en cuenta variables del ámbito exterior. Por ejemplo:

string prefix = "Task";
var filtered = tasks.Where(t => t.Name.StartsWith(prefix));

Aquí la lambda en Where recuerda el valor de prefix y llama al método StartsWith.

7. Particularidades, limitaciones y errores típicos al trabajar con closures

Error nº1: usar una sola variable del bucle para todos los delegates.
Si en el bucle todos los delegates refieren la misma variable, el resultado será inesperado. Es importante crear una nueva variable local dentro del bucle para cada delegate y evitar la referencia compartida.

Error nº2: closures sobre variables fuera del método.
Si un closure captura un campo de clase o una variable declarada fuera del método actual, mantendrá referencia a esa variable. Eso puede provocar fugas de memoria, porque el garbage collector no podrá liberar el objeto mientras existan referencias desde closures.

Error nº3: delegates de larga duración con closures.
Si un delegate con un closure se conserva mucho tiempo (por ejemplo en un campo estático), las variables a las que referencia también permanecerán en memoria más tiempo del esperado. A menudo eso causa fugas de memoria ocultas y problemas de rendimiento.

2
Tarea
C# SELF, nivel 49, lección 4
Bloqueada
Cierre simple
Cierre simple
1
Cuestionario/control
Expresiones lambda, nivel 49, lección 4
No disponible
Expresiones lambda
Sintaxis de expresiones lambda
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION