¿Qué son los 'cierres' en .NET?


195

¿Qué es un cierre? ? ¿Los tenemos en .NET?

Si existen en .NET, ¿podría proporcionar un fragmento de código (preferiblemente en C #) que lo explique?

Respuestas:


258

Tengo un artículo sobre este mismo tema . (Tiene muchos ejemplos).

En esencia, un cierre es un bloque de código que se puede ejecutar en un momento posterior, pero que mantiene el entorno en el que se creó por primera vez, es decir, todavía puede usar las variables locales, etc. del método que lo creó, incluso después de eso. El método ha terminado de ejecutarse.

La característica general de los cierres se implementa en C # mediante métodos anónimos y expresiones lambda.

Aquí hay un ejemplo usando un método anónimo:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement; 
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }
}

Salida:

counter=1
counter=2

Aquí podemos ver que la acción devuelta por CreateAction todavía tiene acceso a la variable del contador y, de hecho, puede incrementarla, aunque CreateAction haya finalizado.


57
Gracias Jon Por cierto, ¿hay algo que no sabes en .NET? :) ¿A quién vas cuando tienes preguntas?
Desarrollador

44
Siempre hay más que aprender :) Acabo de terminar de leer CLR a través de C #, muy informativo. Aparte de eso, generalmente le pido a Marc Gravell los árboles de WCF / enlace / expresión, y Eric Lippert por las cosas del lenguaje C #.
Jon Skeet el

2
Me di cuenta de eso, pero sigo pensando que su afirmación de que es "un bloque de código que se puede ejecutar más adelante" es simplemente incorrecto: no tiene nada que ver con la ejecución, más que ver con los valores de las variables y el alcance que la ejecución , per se.
Jason Bunting

11
Diría que los cierres no son útiles a menos que puedan ejecutarse, y el "en un momento posterior" resalta la "rareza" de poder capturar el entorno (que de otro modo podría haberse ido por el tiempo de ejecución). Si solo cita la mitad de la oración, entonces es una respuesta incompleta, por supuesto.
Jon Skeet el

44
@SLC: Sí, counterestá disponible para ser incrementado: el compilador genera una clase que contiene un countercampo, y cualquier código que haga referencia countertermina en una instancia de esa clase.
Jon Skeet

22

Si está interesado en ver cómo C # implementa Closure, lea "Sé la respuesta (es 42) blog"

El compilador genera una clase en segundo plano para encapsular el método anónimo y la variable j

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0} ", this.j);
    }
    public int j;
}

para la función:

static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0} ", j);
                     };
    } 
}

Convirtiéndolo en:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}

Hola, Daniil. Tu respuesta es muy útil y quería ir más allá de tu respuesta y hacer un seguimiento, pero el enlace no funciona. Desafortunadamente, mi googlefu no es lo suficientemente bueno para encontrar a dónde se mudó.
Knox

10

Los cierres son valores funcionales que mantienen valores variables de su alcance original. C # puede usarlos en forma de delegados anónimos.

Para un ejemplo muy simple, tome este código C #:

    delegate int testDel();

    static void Main(string[] args)
    {
        int foo = 4;
        testDel myClosure = delegate()
        {
            return foo;
        };
        int bar = myClosure();

    }

Al final, la barra se establecerá en 4, y el delegado de myClosure se puede pasar para usarlo en otra parte del programa.

Los cierres se pueden usar para muchas cosas útiles, como la ejecución retrasada o para simplificar las interfaces: LINQ se construye principalmente con cierres. La forma más inmediata que resulta útil para la mayoría de los desarrolladores es agregar controladores de eventos a los controles creados dinámicamente: puede usar cierres para agregar comportamiento cuando se instancia el control, en lugar de almacenar datos en otro lugar.


10
Func<int, int> GetMultiplier(int a)
{
     return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2));  //outputs 4
Console.WriteLine(fn2(3));  //outputs 6
Console.WriteLine(fn3(2));  //outputs 6
Console.WriteLine(fn3(3));  //outputs 9

Un cierre es una función anónima pasada fuera de la función en la que se crea. Mantiene cualquier variable de la función en la que se crea que utiliza.


4

Aquí hay un ejemplo artificial para C # que creé a partir de un código similar en JavaScript:

public delegate T Iterator<T>() where T : class;

public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
        var i = 0; 
        return delegate { return (i < x.Count) ? x[i++] : null; };
}

Entonces, aquí hay un código que muestra cómo usar el código anterior ...

var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});

// So, although CreateIterator() has been called and returned, the variable 
// "i" within CreateIterator() will live on because of a closure created 
// within that method, so that every time the anonymous delegate returned 
// from it is called (by calling iterator()) it's value will increment.

string currentString;    
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null

Espero que sea de alguna ayuda.


1
Has dado un ejemplo, pero no ofreciste una definición general. De sus comentarios aquí deduzco que son "más acerca del alcance", pero ¿seguramente hay más que eso?
ladenedge

2

Básicamente, el cierre es un bloque de código que puede pasar como argumento a una función. C # admite cierres en forma de delegados anónimos.

Aquí hay un ejemplo simple: el
método List.Find puede aceptar y ejecutar un fragmento de código (cierre) para encontrar el elemento de la lista.

// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });

Usando la sintaxis de C # 3.0 podemos escribir esto como:

ints.Find(value => value == 1);

1
Odio ser técnico, pero el cierre tiene más que ver con el alcance: un cierre se puede crear de dos maneras diferentes, pero un cierre no es el medio, es el final.
Jason Bunting

2

Un cierre es cuando una función se define dentro de otra función (o método) y utiliza las variables del método principal . Este uso de variables que se ubican en un método y se envuelven en una función definida dentro de él, se denomina cierre.

Mark Seemann tiene algunos ejemplos interesantes de cierres en su publicación de blog donde hace un paralelismo entre la programación funcional y oop.

Y para hacerlo más detallado

var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
    {
        var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
        return File.ReadAllText(path);
    };//the entire process is called a closure.

1

Los cierres son fragmentos de código que hacen referencia a una variable fuera de ellos (desde debajo de ellos en la pila), que podrían llamarse o ejecutarse más tarde (como cuando se define un evento o delegado, y podrían llamarse en algún momento futuro indefinido) ) ... Debido a que la variable externa a la que el fragmento de código hace referencia puede estar fuera de alcance (y de lo contrario se habría perdido), el hecho de que se hace referencia al fragmento de código (llamado cierre) le dice al tiempo de ejecución que "retenga "esa variable en alcance hasta que ya no sea necesaria por el fragmento de código de cierre ...


Como indiqué en la explicación de otra persona: odio ser técnico, pero el cierre tiene más que ver con el alcance: un cierre se puede crear de dos maneras diferentes, pero un cierre no es el medio, es el final.
Jason Bunting

1
Los cierres son relativamente nuevos para mí, por lo que es muy posible que lo malinterprete, pero entiendo el alcance. Mi respuesta se centra en el alcance. Así que me estoy perdiendo lo que su comentario está tratando de corregir ... ¿Qué más puede ser relevante para el alcance sino un trozo de código? (función, método anónimo o lo que sea)
Charles Bretana

¿No es la clave de un cierre que algún "fragmento de código ejecutable" puede acceder a un valor variable o en memoria que sintácticamente está "fuera" de su alcance, después de que esa variable normalmente se haya "salido del alcance" o haya sido destruida ?
Charles Bretana el

Y @Jason, no te preocupes por ser técnico, esta idea de cierre es algo que me tomó un tiempo entender, en largas discusiones con un compañero de trabajo, sobre los cierres de javascript ... pero él era un loco de Lisp y nunca bastante atravesó las abstracciones en sus explicaciones ...
Charles Bretana

0

También he intentado entenderlo, a continuación se encuentran los fragmentos de código para el mismo código en Javascript y C # que muestran el cierre.

  1. Cuenta el número de veces que ha sucedido cada evento o el número de veces que se hace clic en cada botón.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
      d++;
      alert(d);
  }

  return inner;
};

var a = c();
var b = c();

<body>
<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="b()"/>
</body>

C#:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    int b = 0;

    public  void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
  1. contar el número total de veces que se ha producido un evento de clic o contar el número total de clics independientemente del control.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
     d++;
     alert(d);
  }

  return inner;
};

var a = c();

<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="a()"/>

C#:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    static int b = 0;

    public void call()
    {
      b++;
     Console.WriteLine(b);
    }
}

0

De la nada, una respuesta simple y más comprensiva del libro C # 7.0.

Requisito previo que debe saber : una expresión lambda puede hacer referencia a las variables y parámetros locales del método en el que se define (variables externas).

    static void Main()
    {
    int factor = 2;
   //Here factor is the variable that takes part in lambda expression.
    Func<int, int> multiplier = n => n * factor;
    Console.WriteLine (multiplier (3)); // 6
    }

Parte real : las variables externas a las que hace referencia una expresión lambda se denominan variables capturadas. Una expresión lambda que captura variables se llama cierre.

Último punto a tener en cuenta : las variables capturadas se evalúan cuando se invoca al delegado, no cuando se capturan las variables:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30

0

Si escribe un método anónimo en línea (C # 2) o (preferiblemente) una expresión Lambda (C # 3 +), todavía se está creando un método real. Si ese código está usando una variable local de alcance externo, de todos modos necesita pasar esa variable al método.

por ejemplo, tome esta cláusula Linq Where (que es un método de extensión simple que pasa una expresión lambda):

var i = 0;
var items = new List<string>
{
    "Hello","World"
};   
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time 
{
    i++;
    return true;
});

si desea usar i en esa expresión lambda, debe pasarlo a ese método creado.

Entonces, la primera pregunta que surge es: ¿se debe pasar por valor o referencia?

Pasar por referencia es (supongo) más preferible a medida que obtienes acceso de lectura / escritura a esa variable (y esto es lo que hace C #; supongo que el equipo de Microsoft sopesó los pros y los contras y fue con referencia; según Jon Skeet's artículo , Java fue con by-value).

Pero luego surge otra pregunta: ¿Dónde asignar ese yo?

¿Debería asignarse realmente / naturalmente en la pila? Bueno, si lo asigna en la pila y lo pasa por referencia, puede haber situaciones en las que sobreviva su propio marco de pila. Toma este ejemplo:

static void Main(string[] args)
{
    Outlive();
    var list = whereItems.ToList();
    Console.ReadLine();
}

static IEnumerable<string> whereItems;

static void Outlive()
{
    var i = 0;
    var items = new List<string>
    {
        "Hello","World"
    };            
    whereItems = items.Where(x =>
    {
        i++;
        Console.WriteLine(i);
        return true;
    });            
}

La expresión lambda (en la cláusula Where) nuevamente crea un método que se refiere a una i. Si i está asignado en la pila de Outlive, para cuando enumere whereItems, el i utilizado en el método generado apuntará a i de Outlive, es decir, a un lugar en la pila que ya no es accesible.

Ok, entonces lo necesitamos en el montón entonces.

Entonces, lo que el compilador de C # hace para admitir este anónimo / lambda en línea es usar lo que se llama " Closures ": crea una clase en el montón llamada ( bastante mal DisplayClass ) que tiene un campo que contiene el i, y la función que realmente usa eso.

Algo que sería equivalente a esto (puede ver la IL generada usando ILSpy o ILDASM):

class <>c_DisplayClass1
{
    public int i;

    public bool <GetFunc>b__0()
    {
        this.i++;
        Console.WriteLine(i);
        return true;
    }
}

Crea una instancia de esa clase en su ámbito local y reemplaza cualquier código relacionado con yo o la expresión lambda con esa instancia de cierre. Entonces, cada vez que use la i en su código de "ámbito local" donde estaba definida, en realidad está usando ese campo de instancia DisplayClass.

Entonces, si cambiara el "local" i en el método principal, en realidad cambiará _DisplayClass.i;

es decir

var i = 0;
var items = new List<string>
{
    "Hello","World"
};  
var filtered = items.Where(x =>
{
    i++;
    return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10;            // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12

imprimirá 12, ya que "i = 10" va a ese campo de clase de muestra y lo cambia justo antes de la segunda enumeración.

Una buena fuente sobre el tema es este módulo de Bart De Smet Pluralsight (requiere registro) (también ignora su uso erróneo del término "elevación" - lo que (creo) que quiere decir es que la variable local (es decir, i) se cambia para referirse al nuevo campo DisplayClass).


En otras noticias, parece haber una idea errónea de que los "cierres" están relacionados con los bucles, ya que entiendo que los "cierres" NO son un concepto relacionado con los bucles , sino más bien con métodos anónimos / expresiones lambda que utilizan variables de ámbito local, aunque algunos trucos las preguntas usan bucles para demostrarlo.


-1

Un cierre es una función, definida dentro de una función, que puede acceder a las variables locales de la misma, así como a su padre.

public string GetByName(string name)
{
   List<things> theThings = new List<things>();
  return  theThings.Find<things>(t => t.Name == name)[0];
}

entonces la función dentro del método find.

 t => t.Name == name

puede acceder a las variables dentro de su alcance, t, y el nombre de la variable que está en su alcance primario. Aunque el método find lo ejecuta como delegado, desde otro ámbito, todos juntos.


2
Un cierre no es una función, per se, se define más hablando de alcance que de funciones. Las funciones simplemente ayudan a mantener el alcance, lo que hace que se cree un cierre. Pero decir que un cierre es una función no es técnicamente correcto. Perdón por molestar. :)
Jason Bunting
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.