2011-11-10 26 views
9

Basado en following question, encontré un comportamiento extraño del compilador C#.rareza del compilador C# con delegados constructores

El siguiente es válida # C:

static void K() {} 

static void Main() 
{ 
    var k = new Action(new Action(new Action(K)))); 
} 

Lo que parece extraño es el compilador 'deconstruir' el delegado pasado.

salida El ILSpy es como sigue:

new Action(new Action(new Action(null, ldftn(K)), ldftn(Invoke)).Invoke); 

Como uno puede ver, decide automáticamente para utilizar el método de la delegado Invoke. ¿Pero por qué?

Tal como está, el código no está claro. ¿Tenemos un delegado triplemente envuelto (real) o el delegado interno simplemente 'copiado' a los externos (mi pensamiento inicial).

Sin duda, si la intención era como el compilador emite el código, se debería haber escrito:

var k = new Action(new Action(new Action(K).Invoke).Invoke); 

similar al código descompilación.

¿Alguien puede justificar el motivo de esta transformación "sorprendente"?

Actualización:

sólo puedo pensar en un posible caso de uso para esto; delegar la conversión de tipo. Por ejemplo:

delegate void Baz(); 
delegate void Bar(); 
... 
var k = new Baz(new Bar(new Action (K))); 

Tal vez el compilador debe emitir una advertencia si se utilizan los mismos tipos de delegado.

+2

No está claro qué quiere decir con "es el delegado interior sólo 'copiado 'a los exteriores' - ¿qué es exactamente lo que esperabas? ¿Una única instancia de delegado? –

+1

Fuera de interés, ¿dónde emplearías tal construcción? Parece que el "delegado triplemente envuelto" se comerá al programador poco astuto para el desayuno ... – MoonKnight

+0

@JonSkeet: esperaba que fuera más parecido a 'nueva Acción (a.Target, a.Método)' en lugar de 'nueva Acción (a.Invocar)'. IOW, sin envoltura. – leppie

Respuesta

5

La especificación (sección 7.6.10.5) dice:

  • La nueva instancia delegada es inicializado con la misma lista de invocación como la instancia de delegado dada por E.

Ahora supongamos que el compilador lo tradujo a algo similar a su sugerencia de:

new Action(a.Target, a.Method) 

Eso solo crearía un delegado con una lista de invocación de una llamada de método simple. Para un delegado multi-cast, violaría la especificación.

Código de ejemplo:

using System; 

class Program 
{ 
    static void Main(string[] args) 
    { 
     Action first =() => Console.WriteLine("First"); 
     Action second =() => Console.WriteLine("Second"); 

     Action both = first + second; 
     Action wrapped1 = 
      (Action) Delegate.CreateDelegate(typeof(Action), 
              both.Target, both.Method); 
     Action wrapped2 = new Action(both); 

     Console.WriteLine("Calling wrapped1:"); 
     wrapped1(); 

     Console.WriteLine("Calling wrapped2:"); 
     wrapped2(); 
    } 
} 

Salida:

Calling wrapped1: 
Second 
Calling wrapped2: 
First 
Second 

Como se puede ver, la verdadera comportamiento del compilador coincide con la especificación - su comportamiento sugiere no lo hace.

Esto es en parte debido al poco extraño "a veces un solo molde, a veces de multidifusión" naturaleza de Delegate, por supuesto ...

+0

Espera, ese texto es confuso. El nuevo delegado solo será un contenedor y solo tendrá un único elemento en la lista de invocación. De lo contrario, todo se llamaría dos veces. – leppie

+0

Veo, sin embargo, que los delegados de multidifusión hacen que este sea un escenario complejo. – leppie

+0

'Console.WriteLine (wrapped2.GetInvocationList(). Length);' prints 1. La especificación se infringe (o simplemente se especifica incorrectamente). – leppie

2
  • delegado es una clase
  • Acción delegado tiene un constructor al igual que

    acción pública extern (objeto @object, método IntPtr);

  • Desde K es un método estático que no hay necesidad de pasar objeto a más interior instancia de acción como primer argumento y por lo tanto pasa nulo

  • Desde segundo argumento es puntero para funcionar por lo tanto pasa puntero de método K usando
  • función
  • ldftn para las instancias de acción restantes se pasa el objeto es la acción interior y el segundo parámetro es el método Invoke ya que cuando se llama a un delegado que en realidad estás llamando al método Invoke

Resumen

var action = new Action(K) => Action action = new Action(null, ldftn(K)) 
new Action(action) => new Action(action, ldftn(Action.Invoke)) 

Espero que esto explique lo que está sucediendo?

+0

La pregunta que hago es exactamente lo que dice en su último punto. Sé lo que está sucediendo, pero ¿por qué? – leppie

+0

@leppie check updated answer –

+0

Si vuelve a leer mi pregunta, notará que todo lo que dice ya está allí. Entiendo ** qué ** está sucediendo. Quiero saber cuál es la justificación (o el por qué) para esto. – leppie

3

Cuando intenta tratar a un delegado como un método, el compilador realmente utiliza el método Invoke() del delegado.Así, por ejemplo, las dos líneas por debajo de compilar exactamente a la misma IL (ambas llamadas Invoke()):

k(); 
k.Invoke(); 

Asumo la rareza que se está viendo es una consecuencia de esto. El constructor delegado espera un método (o más bien, un grupo de métodos), pero en su lugar obtiene un delegado. Por lo tanto, lo trata como un método y utiliza el método Invoke().

En cuanto al significado, es el delegado que llama al delegado que llama al método real. Puede verificarlo usted mismo accediendo a las propiedades Method y Target del delegado. En el caso del delegado externo, Method es Action.Invoke y Target el delegado interno.

+1

Sigo un poco, pero este diseño hace que los delegados sean más ciudadanos de segunda clase. Como en algún contexto, 'k' es realmente' k.Invoke', pero en otros casos 'k' es solo un delegado. – leppie

+0

Creo que eso se debe a que los delegados son realmente (casi) clases normales, en lo que respecta al CLR. Para convertirlos en ciudadanos de primera clase en C#, debes recurrir a cosas como esta. – svick

+0

Las clases normales son ciudadanos de primera clase, proporcionando alguna funcionalidad mágica que las hace de segunda clase (bien esa funcionalidad específica). – leppie