2009-09-14 19 views
12

Consideremos el siguiente código de ejemplo:La creación de dos instancias delegar en el mismo método anónimo no son iguales

static void Main(string[] args) 
{ 
    bool same = CreateDelegate(1) == CreateDelegate(1); 
} 

private static Action CreateDelegate(int x) 
{ 
    return delegate { int z = x; }; 
} 

Se podría imaginar que las dos instancias de delegado compararía a ser igual, tal como lo harían cuando se utiliza el buen método de método antiguo (nueva Acción (MiMétodo)). No se comparan para ser iguales porque .NET Framework proporciona una instancia de cierre oculta por instancia de delegado. Como esas dos instancias de delegado tienen sus propiedades de Destino configuradas en su instancia individual oculta, no se comparan. Una posible solución es para el IL generado para un método anónimo para almacenar la instancia actual (este puntero) en el destino del delegado. Esto permitirá que los delegados se puedan comparar correctamente, y también ayuda desde el punto de vista del depurador, ya que verá que su clase es el objetivo, en lugar de una clase oculta.

Puedes leer más sobre este tema en el error que envié a Microsoft. El informe de fallas también brinda un ejemplo de por qué estamos usando esta funcionalidad y por qué creemos que debería cambiarse. Si crees que esto también es un problema, ayúdanos a apoyarlo proporcionando calificación y validación.

https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=489518

hay algún razones posibles por la funcionalidad no debe ser cambiado? ¿Siente que este fue el mejor curso de acción para resolver el problema, o recomienda que tome una ruta diferente?

+2

En qué especificación de qué Microsoft mencionar que estos dos deben ser iguales? Con el mismo razonamiento, puedo argumentar que 'new MyClass() == new MyClass()' debería ser 'true' si' MyClass' es una clase vacía, por ejemplo. Eres demasiado rápido para nombrar un comportamiento que no se menciona en ninguna especificación como "error". –

+0

¿En qué especificación mencionaron que no deberían ser iguales? –

Respuesta

19

No estoy tan inclinado a pensar que esto es un "error". Además, parece que estás asumiendo algún comportamiento en el CLR que simplemente no existe.

Lo importante que debe entender aquí es que está devolviendo un nuevo método anónimo (y inicializando una nueva clase de cierre) cada vez que llame al método CreateDelegate. Parece que está experimentando la palabra clave delegate para usar algún tipo de agrupación para métodos anónimos internamente. El CLR ciertamente no hace esto. Un delegado al método anónimo (como con una expresión lambda) se crea en la memoria cada vez que llama al método, y dado que el operador de igualdad, por supuesto, compara las referencias en esta situación, es el resultado esperado devolver false.

Aunque su comportamiento sugerido puede tener algunos beneficios en ciertos contextos, probablemente sería bastante complicado de implementar, y probablemente conduciría a escenarios impredecibles. Creo que el comportamiento actual de generar un nuevo método anónimo y delegar en cada llamada es el correcto, y sospecho que estos son los comentarios que recibirás también en Microsoft Connect.

Si es bastante insistente en tener el comportamiento que describió en su pregunta, siempre existe la opción de memoizing su función CreateDelegate, que aseguraría que se devuelva el mismo delegado cada vez para los mismos parámetros. De hecho, debido a que es tan fácil de implementar, es probablemente una de las razones por las cuales Microsoft no consideró implementarlo en el CLR.

+0

Las expresiones Lambda no tienen nada que ver con esto. Mira el IL. No estoy sugiriendo ningún tipo de agrupación loca ni nada por el estilo. Simplemente estoy sugiriendo que dejando de lado la implementación, ¿cómo lo haría, como desarrollador que escribe el código, quiere que funcione sabiendo lo que sabe acerca de la comparación de un delegado? Olvídese de los detalles de implementación de métodos anónimos. Simplemente quiero cambiarlo para mejor de la comunidad de desarrolladores y el Framework. –

+0

@BigUnit: las expresiones Lambda tienen mucho que ver con esto. Es precisamente porque se generan sobre la marcha que estás experimentando esta confusión. Personalmente, estoy con los desarrolladores de .NET Framework en este caso. Si escribí el código proporcionado en la pregunta anterior, el resultado real sería mi resultado esperado. Por supuesto, uno difícilmente puede clasificar este comportamiento "intuitivo", pero creo que verá que es un comportamiento perfectamente razonable una vez que se ajuste a su funcionamiento interno del CLR. – Noldorin

+0

@Noldorin: Lo siento, todavía no veo dónde encaja la expresión lambda. No hay LE en mi código, ni en el IL producido. Ciertamente no hay tiempo de ejecución sobre la marcha de la generación, si eso es lo que está consiguiendo. –

4

No conozco los detalles específicos de C# de este problema, pero trabajé en la característica equivalente de VB.Net que tiene el mismo comportamiento.

La conclusión es que este comportamiento es "por diseño" por las siguientes razones

La primera es que en este escenario un cierre es inevitable. Usó un fragmento de datos locales dentro de un método anónimo y, por lo tanto, es necesario cerrarlo para capturar el estado. Cada llamada a este método debe crear un nuevo cierre por varias razones.Por lo tanto, cada delegado apuntará a un método de instancia en ese cierre.

Debajo del capó, un método/expresión anónimo se representa mediante una instancia derivada System.MulticastDelegate en el código. Si nos fijamos en el método equals de esta clase se dará cuenta de 2 detalles importantes

  • Se sella así que no hay forma de que un delegado derivados para cambiar el igual comportamiento
  • parte del método es igual a no una referencia comparación en los objetos

Esto hace que sea imposible que 2 expresiones lambda que están unidas a diferentes cierres se puedan comparar como iguales.

+0

Acepto que hoy el Framework hace que sea imposible comparar los métodos anónimos. Solo porque señalan dos instancias diferentes no significa que no puedan compararse para ser equivalentes. Claro que significaría un cambio, pero ciertamente no diría que sea imposible tan rápido. –

+0

@BigUnit, digo que es imposible dado el estado actual del BCL. Al mismo tiempo, también estoy de acuerdo en que el comportamiento del BCL es correcto. Dos métodos de instancia en dos objetos nunca deberían compararse para ser iguales. – JaredPar

+0

@JaredPar: Entiendo lo que dices, pero está abstraído de ti. Olvidó mencionar que los dos objetos están ocultos para el desarrollador. El hecho de que captura el estado es solo para hacerlo más fácil para el desarrollador. Dame un buen ejemplo de código existente que se rompería de cambiar esta funcionalidad? Es una lástima que no hay una buena solución a este problema que lo haga simple para la persona que usa la API, excepto que no use métodos anónimos y se vea forzado a usar el paradigma Func/Action. Entonces me vería obligado a crear el mismo número de clases genéricas en mi situación, alrededor de 30. –

0

No puedo pensar en una situación en la que alguna vez haya necesitado hacer eso. Si tengo que comparar los delegados siempre uso delegados nombrados, de lo contrario algo como esto sería posible:

MyObject.MyEvent += delegate { return x + y; }; 

MyObject.MyEvent -= delegate { return x + y; }; 

Este ejemplo no es muy grande para demostrar el problema, pero me imagino que podría haber una situación en la que permite esto podría romper el código existente que fue diseñado con la expectativa de que esto no está permitido.

Estoy seguro de que hay detalles de implementación interna que también hacen que esta sea una mala idea, pero no sé exactamente cómo se implementan internamente los métodos anónimos.

+0

Actualmente, nadie hace algo así porque los resultados generalmente serían falsos. Pero aquí hay un caso donde el Marco permite tal resultado: Si tengo un método como este: público Acción CreateDelegate() { return delegate {CallSomeMethod(); }; } Y a continuación, utilice esa función para mi evento de conexión/eliminación: MyEvent + = CreateDelegate(); MyEvent - = CreateDelegate(); Funciona sin problemas. Incluso actualmente hay casos en que les permitirán ser equivalentes. Entonces, no hay forma de escribir código para determinar la equivalencia hoy. –

3

EDIT: respuesta vieja izquierda de valor histórico por debajo de la línea de ...

El CLR tendría que trabajar en los casos en los que las clases ocultos podrían ser considerados iguales, teniendo en cuenta todo lo que se podía hacer con las variables capturadas

En este caso particular, la variable capturada (x) no se cambia ni en el delegado ni en el contexto de captura, pero prefiero que el lenguaje no requiera este tipo de complejidad de análisis. Cuanto más complicado es el lenguaje, más difícil es de entender. Tendría que distinguir entre este caso y el siguiente, donde el valor de la variable capturada se cambia en cada invocación; allí, hace una gran diferencia que delegue su llamada; de ninguna manera son iguales.

Creo que es completamente sensato que esta situación ya compleja (los cierres no se entiendan con frecuencia) no trata de ser demasiado "inteligente" y resolver la posible igualdad.

IMO, debe definitivamente tomar una ruta diferente. Estos son conceptualmente instancias independientes de Action. Fingir coerciendo los objetivos de los delegados es un horrible truco IMO.


El problema es que está capturando el valor de x en una clase generada. Las dos variables x son independientes, por lo que son delegados desiguales.He aquí un ejemplo que demuestra la independencia:

using System; 

class Test 
{ 
    static void Main(string[] args) 
    { 
     Action first = CreateDelegate(1); 
     Action second = CreateDelegate(1); 
     first(); 
     first(); 
     first(); 
     first(); 
     second(); 
     second(); 
    } 

    private static Action CreateDelegate(int x) 
    { 
     return delegate 
     { 
      Console.WriteLine(x); 
      x++; 
     }; 
    } 
} 

Salida:

1 
2 
3 
4 
1 
2 

EDIT: Para verlo de otra manera, su programa original era el equivalente de:

using System; 

class Test 
{ 
    static void Main(string[] args) 
    { 
     bool same = CreateDelegate(1) == CreateDelegate(1); 
    } 

    private static Action CreateDelegate(int x) 
    { 
     return new NestedClass(x).ActionMethod; 
    } 

    private class Nested 
    { 
     private int x; 

     internal Nested(int x) 
     { 
      this.x = x; 
     } 

     internal ActionMethod() 
     { 
      int z = x; 
     } 
    } 
} 

Como se puede contar, dos instancias separadas de Nested se crearán, y serán los objetivos para los dos delegados. Son desiguales, por lo que los delegados también son desiguales.

+0

Creo que el OP entiende esto (lo suficientemente bien), y más bien su pregunta principal se relaciona con * why * este es el comportamiento del CLR. – Noldorin

+0

@Noldorin: Sí, he vuelto a leer la pregunta y he respondido. –

+0

Entiendo por qué actualmente hace lo que hace. Usted dijo: "Cuanto más complicado es el lenguaje, más difícil es de entenderlo". Sin embargo, no propongo ningún cambio en el lenguaje en sí, sino en el IL generado subyacente que ya se ha realizado a través del compilador. En cierto sentido, el Marco está hecho para facilitar la vida, entonces, ¿por qué hacerlo más difícil cuando no es necesario? Cambiar el objetivo no es la mejor respuesta, es necesario que haya una mejor manera. Podría almacenarlo como un campo diferente en el delegado, o la clase de captura podría almacenarlo de una manera que el delegado podría acceder para la comparación. –

0

Este comportamiento tiene sentido porque de lo contrario los métodos anónimos se mezclarían (si tuvieran el mismo nombre, dado el mismo cuerpo).

Se podría cambiar el código para esto:

static void Main(){ 
    bool same = CreateDelegate(1) == CreateDelegate(1); 
} 

static Action<int> action = (x) => { int z = x; }; 

private static Action<int> CreateDelegate(int x){ 
    return action; 
} 

o, preferiblemente, ya que eso es una mala manera de utilizarlo (además de que estaban comparando el resultado, y la acción no tiene un valor de retorno .. . Func utilizar < ...> si desea devolver un valor):

static void Main(){ 
    var action1 = action; 
    var action2 = action; 
    bool same = action1 == action2; // TRUE, of course 
} 

static Action<int> action = (x) => { int z = x; }; 
Cuestiones relacionadas