Por que algumas expressões lambda do C # são compiladas para métodos estáticos?

122

Como você pode ver no código abaixo, declarei um Action<>objeto como uma variável.

Alguém poderia me informar por que esse delegado do método de ação se comporta como um método estático?

Por que ele retorna trueno código a seguir?

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();
}

Resultado:

exemplo de saída da amostra

nunu
fonte

Respostas:

153

Isso é mais provável porque não há fechamentos, por exemplo:

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);

Isso produzirá falsepara withClosuree truepara withoutClosure.

Quando você usa uma expressão lambda, o compilador cria uma pequena classe para conter seu método, isso é compilado para algo como o seguinte (a implementação real provavelmente varia um pouco):

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);
}

Você pode ver que as Action<string>instâncias resultantes realmente apontam para métodos nessas classes geradas.

Lukazoid
fonte
4
+1. Pode confirmar - sem um fechamento, eles são candidatos perfeitos para staticmétodos.
11136 Simon Whitehead #
3
Eu estava apenas sugerindo que esta pergunta precisava de alguma expansão, voltei e lá estava. Muito informativo - ótimo para ver o que o compilador está fazendo nos bastidores.
Liath
4
O @Liath Ildasmé realmente útil para entender o que realmente está acontecendo; costumo usar a ILguia LINQPadpara examinar pequenas amostras.
Lukazoid
@Lukazoid Você poderia nos dizer como conseguiu essa saída do compilador? O ILDASM não fornecerá essa saída. Por qualquer ferramenta ou software?
Nunu
8
@nunu Neste exemplo, usei a ILguia LINQPade inferi o C #. Algumas opções para obter o equivalente C # real da saída compilada seriam usar ILSpyou Reflectorno assembly compilado, você provavelmente precisará desativar algumas opções que tentarão exibir as lambdas e não as classes geradas pelo compilador.
Lukazoid
20

O "método de ação" é estático apenas como efeito colateral da implementação. Este é um caso de um método anônimo sem variáveis ​​capturadas. Como não há variáveis ​​capturadas, o método não possui requisitos de vida útil adicionais além daqueles para variáveis ​​locais em geral. Se ele fez referência a outras variáveis ​​locais, sua vida útil se estende à vida útil dessas outras variáveis ​​(consulte a seção L.1.7, Variáveis ​​locais e a seção N.15.5.1, Variáveis ​​externas capturadas , na especificação C # 5.0).

Observe que a especificação C # fala apenas sobre métodos anônimos sendo convertidos em "árvores de expressão", não em "classes anônimas". Embora a árvore de expressão possa ser representada como classes C # adicionais, por exemplo, no compilador da Microsoft, essa implementação não é necessária (conforme reconhecido pela seção M.5.3 na especificação C # 5.0). Portanto, não está definido se a função anônima é estática ou não. Além disso, a seção K.6 deixa muito aberto quanto aos detalhes das árvores de expressão.

Peter O.
fonte
2
+1 é provável que esse comportamento não deva ser considerado, pelas razões declaradas; é um detalhe de implementação.
Lukazoid
18

O comportamento do cache de delegado foi alterado em Roslyn. Anteriormente, como declarado, qualquer expressão lambda que não capturasse variáveis ​​foi compilada em um staticmétodo no site de chamada. Roslyn mudou esse comportamento. Agora, qualquer lambda, que captura variáveis ​​ou não, é transformado em uma classe de exibição:

Dado este exemplo:

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

Saída do 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);
        }
    }
}

Delegar alterações de comportamento de cache em Roslyn fala sobre por que essa alteração foi feita.

Yuval Itzchakov
fonte
2
Obrigado, eu estava me perguntando por que meu Func <int> f = () => 5 'método s não era estática
vc 74
1

O método não tem fechamentos e também faz referência a um método estático (Console.WriteLine), portanto, espero que seja estático. O método declarará um tipo anônimo de fechamento para um fechamento, mas, neste caso, não é necessário.

Mel Padden
fonte