2011-01-26 11 views
15

Las cadenas son tipos de referencia, pero son inmutables. Esto les permite ser internados por el compilador; en todas partes aparece el mismo literal de cadena, el mismo objeto puede ser referenciado.¿Por qué las expresiones lambda no son "internas"?

Los delegados también son tipos de referencia inmutables. (Agregar un método a un delegado de multidifusión usando el operador += constituye asignación; eso no es mutabilidad). Y, como cadenas, hay una forma "literal" de representar a un delegado en el código, usando una expresión lambda, por ejemplo:

Func<int> func =() => 5; 

El lado derecho de esa declaración es una expresión cuyo tipo es Func<int>; pero en ninguna parte estoy invocando explícitamente el constructor Func<int> (ni está ocurriendo una conversión implícita). Así que veo esto como esencialmente un literal. ¿Estoy equivocado sobre mi definición de "literal" aquí?

De todos modos, aquí está mi pregunta. Si tengo dos variables para, por ejemplo, el tipo Func<int>, y asigno expresiones lambda idénticos a ambos:

Func<int> x =() => 5; 
Func<int> y =() => 5; 

... lo que está impidiendo el compilador de tratar a estos como el mismo Func<int> objeto?

Lo pregunto porque la sección 6.5.1 de la C# 4.0 language specification indica claramente:

Conversiones de semánticamente idénticos funciones anónimas con el mismo conjunto (posiblemente vacío) de instancias de variables externas capturados a la misma delegado tipos están permitidos (pero no se requiere ) para devolver la misma instancia delegado . El término semánticamente idéntico se utiliza aquí para indicar que la ejecución de las funciones anónimas producirá, en todos los casos, los mismos efectos dados los mismos argumentos.

Esto me sorprendió cuando lo leí; si este comportamiento es explícitamente permitido, habría esperado que se implementara. Pero parece que no lo es. Esto de hecho ha metido a muchos desarrolladores en problemas, esp. cuando las expresiones lambda se han utilizado para adjuntar controladores de eventos con éxito sin poder eliminarlos. Por ejemplo:

class EventSender 
{ 
    public event EventHandler Event; 
    public void Send() 
    { 
     EventHandler handler = this.Event; 
     if (handler != null) { handler(this, EventArgs.Empty); } 
    } 
} 

class Program 
{ 
    static string _message = "Hello, world!"; 

    static void Main() 
    { 
     var sender = new EventSender(); 
     sender.Event += (obj, args) => Console.WriteLine(_message); 
     sender.Send(); 

     // Unless I'm mistaken, this lambda expression is semantically identical 
     // to the one above. However, the handler is not removed, indicating 
     // that a different delegate instance is constructed. 
     sender.Event -= (obj, args) => Console.WriteLine(_message); 

     // This prints "Hello, world!" again. 
     sender.Send(); 
    } 
} 

¿Hay alguna razón por la cual este comportamiento, una instancia de delegado para anónimas semánticamente idénticos métodos, no ha aplicado?

+4

Incluso si funcionó, todavía creo que es una mala idea separar un controlador de eventos duplicando el código en el lambda. –

+2

Sospecho que este será otro de esos "porque el presupuesto no estaba disponible para diseñar, implementar, probar, documentar, enviar y mantener" agradables para tener. Tal vez Eric Lippert pueda sonar con información privilegiada. – LukeH

+0

Buena pregunta. Yo postularía que una lambda como se ve en el código puede parecer idéntica a otra, pero debido a las diferencias referenciales como los cierres externos no se compilaría en el mismo MSIL en ambos lugares. El compilador tiene que compilarlo en MSIL para detectar la diferencia, en lugar de poder detectar un literal de cadena "sobre la marcha" desde el código fuente. Como este es un paso adicional que se requeriría para cualquier lambda, y solo proporciona un ahorro de tamaño pequeño y poco o ningún aumento en el rendimiento, probablemente se lo saltaron. – KeithS

Respuesta

11

Se equivoca al llamarlo literal, IMO. Es solo una expresión que es convertible a un tipo de delegado.

Ahora en cuanto a la "internar" parte - algunas expresiones lambda son en caché, en la que por una sola expresión lambda, a veces una sola instancia pueden ser creados y reutilizados sin embargo a menudo se encontró con esa línea de código. Algunos no son tratados de esa manera: generalmente depende de si la expresión lambda captura cualquier variable no estática (ya sea a través de "esto" o local al método).

He aquí un ejemplo de este almacenamiento en caché:

using System; 

class Program 
{ 
    static void Main() 
    { 
     Action first = GetFirstAction(); 
     first -= GetFirstAction(); 
     Console.WriteLine(first == null); // Prints True 

     Action second = GetSecondAction(); 
     second -= GetSecondAction(); 
     Console.WriteLine(second == null); // Prints False 
    } 

    static Action GetFirstAction() 
    { 
     return() => Console.WriteLine("First"); 
    } 

    static Action GetSecondAction() 
    { 
     int i = 0; 
     return() => Console.WriteLine("Second " + i); 
    } 
} 

En este caso podemos ver que se almacena en caché la primera acción (o al menos, se produjeron dos iguales delegados, y de hecho Reflector muestra que realmente es en caché en un campo estático). La segunda acción creó dos instancias desiguales de Action para las dos llamadas a GetSecondAction, por lo que "segundo" no es nulo al final.

Las lambdas de internación que aparecen en lugares diferentes en el código pero con el mismo código fuente son una cuestión diferente. Sospecho que sería bastante complejo hacer esto correctamente (después de todo, el mismo código fuente puede significar cosas diferentes en diferentes lugares) y ciertamente no quisiera confiar en que se lleva a cabo. Si no valdrá la pena depender de eso, y es mucho trabajo hacer bien para el equipo compilador, no creo que sea la mejor forma en que podrían estar pasando su tiempo.

+0

"el mismo código fuente" no es "semánticamente idéntico" como se define en la especificación. '(x, y) => x + y' podría ser" semánticamente idéntico "a' (a, b) => a + b' bajo ciertas circunstancias (no local vars, no this, same types, etc). Entiendo que esta equivalencia semántica podría estar llena de casos extremos y ser infernal de detectar, y así conducir a la implementación teniendo en cuenta el enfoque simple. –

+0

() => 5 en un punto en el código fuente no es equivalente a() => 5 en algún otro lugar. Los delegados cedidos por cada uno deben comparar la desigualdad, lo que no sucedería si fueran internados. – supercat

+1

@supercat: No, no lo creo, suponiendo que los tipos de delegados son los mismos, creo que el fragmento de la especificación que se menciona en la pregunta es que * no * se requiere para comparar desigual. –

1

Las lambdas no se pueden internar porque usan un objeto para contener las variables locales capturadas. Y esta instancia es diferente cada vez que construyes el delegado.

+3

No todas las lambdas capturan variables locales. El ejemplo de Dan Tao es uno de estos. –

+1

No todos, pero hay suficientes que la optimización probablemente no valga la pena. Y el código nunca debería depender de que se los interna por su corrección, ya que IMO solo sería una optimización y no por contrato. En particular, si la optimización se implementara, haría que el código OP funcione en algunas implementaciones, pero no en otras. Y de repente deja de funcionar una vez que se capturan las variables. Por lo que probablemente incluso engañar a más desarrolladores que el comportamiento actual. – CodesInChaos

+0

Además, la parte que cité de la especificación indica que si dos expresiones lambda en el mismo método capturan las * mismas * variables locales, entonces son semánticamente idénticas y pueden ser soportadas por la misma instancia de delegado. No estoy hablando de * entre * llamadas de método, eso sí, pero dentro de una sola llamada a un método, las dos expresiones podrían usar el mismo objeto. –

1

En general, no hay distinción entre las variables de cadena que hacen referencia a la misma instancia de Cadena, frente a dos variables que hacen referencia a diferentes cadenas que contienen la misma secuencia de caracteres. Lo mismo no es cierto para los delegados.

Supongamos que tengo dos delegados, asignados a dos expresiones lambda diferentes. Luego suscribo a ambos delegados a un controlador de eventos y anulo la suscripción. ¿Cuál debería ser el resultado?

Sería útil si hubiera una forma en vb o C# para designar que un método anónimo o lambda que no hace referencia a Me/this debería considerarse un método estático, produciendo un único delegado que podría reutilizarse durante toda la vida de la aplicación. Sin embargo, no existe una sintaxis que indique que, para que el compilador decida tener diferentes expresiones lambda, la misma instancia sería un cambio potencial.

EDIT Supongo que la especificación lo permite, a pesar de que podría ser un cambio potencialmente disruptivo si algún código dependiera de que las instancias fueran distintas.

+1

'public static readonly Func TheDeepThoughtLambda =() => 42;': P –

+0

El código que depende de distintas instancias se basa en los detalles de implementación actuales y no tiene en cuenta la especificación. Si se implementara ahora, sin ningún cambio en la especificación, ¿llamaría eso un cambio de ruptura, porque se rompió el código no conforme? –

+0

@Martinho: (Tenga en cuenta mi edición anterior) – supercat

1

Esto está permitido porque el equipo de C# no puede controlar esto. Se basan en gran medida en los detalles de implementación de los delegados (CLR + BCL) y el optimizador del compilador JIT. Ya hay una gran proliferación de implementaciones CLR y jitter en este momento y hay pocas razones para suponer que va a terminar. La especificación CLI es muy liviana en las reglas sobre los delegados, no lo suficientemente fuerte como para asegurar que todos estos equipos diferentes terminen con una implementación que garantice que la igualdad de objetos delegados sea consistente. No en lo más mínimo porque eso obstaculizaría la innovación futura. Hay mucho para optimizar aquí.

6

¿Hay algún motivo por el que no se implemente este comportamiento, una instancia de delegado para métodos anónimos semánticamente idénticos?

Sí.Debido a que perder tiempo en una optimización engañosa que beneficia a casi nadie le quita tiempo para diseñar, implementar, probar y mantener características que hacen beneficia a las personas.

+0

Esto básicamente confirma lo que ya habían sugerido Jon, LukeH y posiblemente otros; Lo que me hizo cuestionar esto fue simplemente que la posibilidad de la característica se mencionaba en la especificación en absoluto. Pero como Jon (y otros) señalaron, esto podría haber sido simplemente para evitar cerrar una puerta innecesariamente. Hans también dio una bonita explicación persuasiva. De todos modos, gracias como siempre por coincidir con lo que supongo que solo se puede llamar la respuesta autorizada! –

Cuestiones relacionadas