tutorial expressions expresiones delegados c# delegates lambda

expresiones - lambda expressions c#



¿Cuál es la vida útil de un delegado creado por un lambda en C#? (5)

Lambdas son agradables, ya que ofrecen brevedad y localidad y una forma adicional de encapsulación . En lugar de tener que escribir funciones que solo se usan una vez, puede usar una lambda.

Mientras me preguntaba cómo funcionaron, intuitivamente pensé que probablemente solo se crearon una vez . Esto me inspiró a crear una solución que permite restringir el alcance de un miembro de la clase más allá de privado a un alcance particular mediante el uso de la lambda como un identificador del ámbito en el que se creó.

Esta implementación funciona, aunque tal vez exagerada (aún lo estoy investigando), lo que demuestra que mi suposición es correcta.

Un ejemplo más pequeño:

class SomeClass { public void Bleh() { Action action = () => {}; } public void CallBleh() { Bleh(); // `action` == {Method = {Void <SomeClass>b__0()}} Bleh(); // `action` still == {Method = {Void <SomeClass>b__0()}} } }

¿Alguna vez la lambda devolvería una nueva instancia o se garantiza que siempre será la misma?


Buena pregunta. No tengo una "respuesta académica", más que una respuesta práctica: pude ver un compilador optimizando el binario para usar la misma instancia, pero nunca escribiría código que suponga que está "garantizado" que es la misma instancia .

Al menos lo voté, así que espero que alguien pueda darle la respuesta académica que está buscando.


No está garantizado de ninguna manera.

Por lo que recuerdo de la implementación actual de MS:

  • Una expresión lambda que no captura ninguna variable se almacena en caché de forma estática
  • Una expresión lambda que solo capture "esto" podría capturarse por instancia, pero no es
  • Una expresión lambda que captura una variable local no se puede almacenar en caché
  • Dos expresiones lambda que tienen exactamente el mismo texto de programa no tienen alias; en algunos casos podrían serlo, pero resolver las situaciones en las que pueden ser sería muy complicado
  • EDITAR: Como señala Eric en los comentarios, también debe considerar los argumentos de tipo que se capturan para los métodos genéricos.

EDITAR: El texto relevante de la especificación C # 4 se encuentra en la sección 6.5.1:

Las conversiones de funciones anónimas semánticamente idénticas con el mismo conjunto (posiblemente vacío) de instancias variables externas capturadas en los mismos tipos de delegados se permiten (pero no se requieren) para devolver la misma instancia delegada. El término semánticamente idéntico se utiliza aquí para indicar que la ejecución de las funciones anónimas, en todos los casos, producirá los mismos efectos dados los mismos argumentos.


Según su pregunta aquí y su comentario a la respuesta de Jon, creo que está confundiendo muchas cosas. Para asegurarse de que esté claro:

  • El método que respalda al delegado para un lambda dado es siempre el mismo.
  • Se permite que el método que respalda al delegado para "el mismo" lambda que aparece léxicamente dos veces sea el mismo, pero en la práctica no es lo mismo en nuestra implementación.
  • La instancia de delegado que se crea para un lambda dado puede o no ser siempre la misma, dependiendo de cuán inteligente sea el compilador para almacenarlo en caché.

Entonces, si tienes algo como:

for(i = 0; i < 10; ++i) M( ()=>{} )

cada vez que se llama a M, se obtiene la misma instancia del delegado porque el compilador es inteligente y genera

static void MyAction() {} static Action DelegateCache = null; ... for(i = 0; i < 10; ++i) { if (C.DelegateCache == null) C.DelegateCache = new Action ( C.MyAction ) M(C.DelegateCache); }

Si usted tiene

for(i = 0; i < 10; ++i) M( ()=>{this.Bar();} )

entonces el compilador genera

void MyAction() { this.Bar(); } ... for(i = 0; i < 10; ++i) { M(new Action(this.MyAction)); }

Obtienes un nuevo delegado cada vez, con el mismo método.

El compilador puede (pero de hecho no lo hace en este momento) generar

void MyAction() { this.Bar(); } Action DelegateCache = null; ... for(i = 0; i < 10; ++i) { if (this.DelegateCache == null) this.DelegateCache = new Action ( this.MyAction ) M(this.DelegateCache); }

En ese caso, siempre obtendría la misma instancia de delegado si fuera posible, y cada delegado estaría respaldado por el mismo método.

Si usted tiene

Action a1 = ()=>{}; Action a2 = ()=>{};

Luego, en la práctica, el compilador genera esto como

static void MyAction1() {} static void MyAction2() {} static Action ActionCache1 = null; static Action ActionCache2 = null; ... if (ActionCache1 == null) ActionCache1 = new Action(MyAction1); Action a1 = ActionCache1; if (ActionCache2 == null) ActionCache2 = new Action(MyAction2); Action a2 = ActionCache2;

Sin embargo, el compilador tiene permitido detectar que las dos lambdas son idénticas y generan

static void MyAction1() {} static Action ActionCache1 = null; ... if (ActionCache1 == null) ActionCache1 = new Action(MyAction1); Action a1 = ActionCache1; Action a2 = ActionCache1;

¿Eso está claro?


Sin garantías

Una demostración rápida:

Action GetAction() { return () => Console.WriteLine("foo"); }

Llame esto dos veces, haga un ReferenceEquals(a,b) , y obtendrá true

Action GetAction() { var foo = "foo"; return () => Console.WriteLine(foo); }

Llama esto dos veces, haz un ReferenceEquals(a,b) , y obtendrás false


Veo que Skeet se incorporó mientras respondía, así que no profundizaré en ese punto. Una cosa que sugeriría, para comprender mejor cómo está usando las cosas, es familiarizarse con las herramientas de ingeniería inversa e IL. Tome las muestras de código en cuestión y aplique ingeniería inversa a IL. Le dará una gran cantidad de información sobre cómo está funcionando el código.