¿Por qué algunas expresiones lambda de C # se compilan con métodos estáticos?

122

Como puede ver en el siguiente código, he declarado un Action<>objeto como una variable.

¿Alguien podría decirme por qué este delegado de método de acción se comporta como un método estático?

¿Por qué vuelve trueen el siguiente código?

Código:

public static void Main(string[] args)
{
    Action<string> actionMethod = s => { Console.WriteLine("My Name is " + s); };

    Console.WriteLine(actionMethod.Method.IsStatic);

    Console.Read();
}

Salida:

ejemplo de salida de muestra

nunu
fuente

Respuestas:

153

Esto es muy probable porque no hay cierres, por ejemplo:

int age = 25;
Action<string> withClosure = s => Console.WriteLine("My name is {0} and I am {1} years old", s, age);
Action<string> withoutClosure = s => Console.WriteLine("My name is {0}", s);
Console.WriteLine(withClosure.Method.IsStatic);
Console.WriteLine(withoutClosure.Method.IsStatic);

Esto generará falsepara withClosurey truepara withoutClosure.

Cuando usa una expresión lambda, el compilador crea una pequeña clase para contener su método, esto se compilaría de forma similar a lo siguiente (la implementación real probablemente varía ligeramente):

private class <Main>b__0
{
    public int age;
    public void withClosure(string s)
    {
        Console.WriteLine("My name is {0} and I am {1} years old", s, age)
    }
}

private static class <Main>b__1
{
    public static void withoutClosure(string s)
    {
        Console.WriteLine("My name is {0}", s)
    }
}

public static void Main()
{
    var b__0 = new <Main>b__0();
    b__0.age = 25;
    Action<string> withClosure = b__0.withClosure;
    Action<string> withoutClosure = <Main>b__1.withoutClosure;
    Console.WriteLine(withClosure.Method.IsStatic);
    Console.WriteLine(withoutClosure.Method.IsStatic);
}

Puede ver que las Action<string>instancias resultantes en realidad apuntan a métodos en estas clases generadas.

Lukazoide
fuente
44
+1. Puede confirmar: sin un cierre, son candidatos perfectos para los staticmétodos.
Simon Whitehead
3
Solo iba a sugerir que esta pregunta necesitaba algo de expansión, regresé y ahí estaba. Muy informativo: excelente para ver qué hace el compilador debajo de las cubiertas.
Liath
44
@Liath Ildasmes realmente útil para comprender lo que realmente está sucediendo, tiendo a usar la ILpestaña de LINQPadpara examinar pequeñas muestras.
Lukazoid
@Lukazoid ¿Podría decirnos cómo obtuvo esta salida del compilador? ILDASM no dará ese resultado. ¿Por alguna herramienta O software?
nunu
8
@nunu En este ejemplo, utilicé la ILpestaña LINQPade inferí C #. Algunas opciones para obtener el equivalente real de C # de la salida compilada sería usar ILSpyo Reflectoren el ensamblado compilado, lo más probable es que necesite deshabilitar algunas opciones que intentarán mostrar las lambdas y no las clases generadas por el compilador.
Lukazoid
20

El "método de acción" es estático solo como un efecto secundario de la implementación. Este es un caso de un método anónimo sin variables capturadas. Como no hay variables capturadas, el método no tiene requisitos adicionales de vida útil más allá de los de las variables locales en general. Si hizo referencia a otras variables locales, su vida útil se extiende a la vida útil de esas otras variables (consulte la sección L.1.7, Variables locales , y la sección N.15.5.1, Variables externas capturadas , en la especificación C # 5.0).

Tenga en cuenta que la especificación C # solo habla de métodos anónimos que se convierten en "árboles de expresión", no en "clases anónimas". Si bien el árbol de expresión podría representarse como clases adicionales de C #, por ejemplo, en el compilador de Microsoft, esta implementación no es necesaria (como se reconoce en la sección M.5.3 en la especificación de C # 5.0). Por lo tanto, no está definido si la función anónima es estática o no. Además, la sección K.6 deja mucho abierto en cuanto a los detalles de los árboles de expresión.

Peter O.
fuente
2
+1 es muy probable que no se confíe en este comportamiento, por las razones indicadas; Es en gran medida un detalle de implementación.
Lukazoid
18

El comportamiento de almacenamiento en caché de delegados cambió en Roslyn. Anteriormente, como se indicó, cualquier expresión lambda que no capturara variables se compilaba en un staticmétodo en el sitio de la llamada. Roslyn cambió este comportamiento. Ahora, cualquier lambda, que captura variables o no, se transforma en una clase de visualización:

Dado este ejemplo:

public class C
{
    public void M()
    {
        var x = 5;
        Action<int> action = y => Console.WriteLine(y);
    }
}

Salida del compilador nativo:

public class C
{
    [CompilerGenerated]
    private static Action<int> CS$<>9__CachedAnonymousMethodDelegate1;
    public void M()
    {
        if (C.CS$<>9__CachedAnonymousMethodDelegate1 == null)
        {
            C.CS$<>9__CachedAnonymousMethodDelegate1 = new Action<int>(C.<M>b__0);
        }
        Action<int> arg_1D_0 = C.CS$<>9__CachedAnonymousMethodDelegate1;
    }
    [CompilerGenerated]
    private static void <M>b__0(int y)
    {
        Console.WriteLine(y);
    }
}

Roslyn

public class C
{
    [CompilerGenerated]
    private sealed class <>c__DisplayClass0
    {
        public static readonly C.<>c__DisplayClass0 CS$<>9__inst;
        public static Action<int> CS$<>9__CachedAnonymousMethodDelegate2;
        static <>c__DisplayClass0()
        {
            // Note: this type is marked as 'beforefieldinit'.
            C.<>c__DisplayClass0.CS$<>9__inst = new C.<>c__DisplayClass0();
        }
        internal void <M>b__1(int y)
        {
            Console.WriteLine(y);
        }
    }
    public void M()
    {
        Action<int> arg_22_0;
        if (arg_22_0 = C.
                       <>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 == null)
        {
            C.<>c__DisplayClass0.CS$<>9__CachedAnonymousMethodDelegate2 =
          new Action<int>(C.<>c__DisplayClass0.CS$<>9__inst.<M>b__1);
        }
    }
}

Los cambios en el comportamiento de almacenamiento en caché de delegados en Roslyn hablan sobre por qué se realizó este cambio.

Yuval Itzchakov
fuente
2
Gracias, me preguntaba por qué mi método Func <int> f = () => 5 no era estático
vc 74
1

El método no tiene cierres y también hace referencia a un método estático en sí mismo (Console.WriteLine), por lo que esperaría que fuera estático. El método declarará un tipo anónimo de cierre para un cierre, pero en este caso no es obligatorio.

Mel Padden
fuente