Dlaczego niektóre wyrażenia lambda języka C # kompilują się do metod statycznych?

122

Jak widać w poniższym kodzie, zadeklarowałem plik Action<> obiekt jako zmienną.

Czy ktoś mógłby mi powiedzieć, dlaczego ten delegat metody akcji zachowuje się jak metoda statyczna?

Dlaczego powraca truew poniższym kodzie?

Kod:

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

    Console.WriteLine(actionMethod.Method.IsStatic);

    Console.Read();
}

Wynik:

przykładowe dane wyjściowe sample

nunu
źródło

Odpowiedzi:

153

Jest to najprawdopodobniej spowodowane brakiem zamknięć, na przykład:

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

To wyświetli falsedla withClosurei truedlawithoutClosure .

Kiedy używasz wyrażenia lambda, kompilator tworzy małą klasę zawierającą twoją metodę, skompilowałoby to do czegoś podobnego do następującego (rzeczywista implementacja najprawdopodobniej nieznacznie się różni):

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

Możesz zobaczyć, że wynikowe Action<string>instancje faktycznie wskazują metody na tych wygenerowanych klasach.

Lukazoid
źródło
4
+1. Potwierdzam - bez domknięcia są idealnymi kandydatami do staticmetod.
Simon Whitehead
3
Chciałem tylko zasugerować, że to pytanie wymaga rozszerzenia, wróciłem i tak było. Bardzo pouczające - świetnie widzieć, co kompilator robi pod okładkami.
Liath
4
@Liath Ildasmjest naprawdę przydatny do zrozumienia, co się właściwie dzieje, zwykle używam ILkarty LINQPaddo badania małych próbek.
Lukazoid
@Lukazoid Czy mógłbyś nam powiedzieć, jak otrzymałeś dane wyjściowe kompilatora? ILDASM nie da takiego wyjścia .. jakimkolwiek narzędziem LUB oprogramowaniem?
nunu
8
@nunu W tym przykładzie użyłem ILkarty LINQPadi wywnioskowałem C #. Niektóre opcje uzyskania rzeczywistego odpowiednika C # skompilowanych danych wyjściowych polegałyby na użyciu ILSpylub Reflectorw skompilowanym zestawie, najprawdopodobniej będziesz musiał wyłączyć niektóre opcje, które będą próbowały wyświetlić lambdy, a nie klasy wygenerowane przez kompilator.
Lukazoid
20

„Metoda akcji” jest statyczna tylko jako efekt uboczny implementacji. Jest to przypadek metody anonimowej bez przechwytywanych zmiennych. Ponieważ nie ma zmiennych przechwytywanych, metoda nie ma dodatkowych wymagań dotyczących czasu życia poza ogólnymi wymaganiami dla zmiennych lokalnych. Jeśli odnosił się do innych zmiennych lokalnych, jego czas życia rozciąga się na czas życia tych innych zmiennych (patrz rozdział L.1.7, Zmienne lokalne i rozdział N.15.5.1, Przechwycone zmienne zewnętrzne w specyfikacji C # 5.0).

Zauważ, że specyfikacja C # mówi tylko o anonimowych metodach konwertowanych na „drzewa wyrażeń”, a nie o „klasy anonimowe”. Chociaż drzewo wyrażeń może być reprezentowane jako dodatkowe klasy C #, na przykład w kompilatorze firmy Microsoft, ta implementacja nie jest wymagana (co potwierdza sekcja M.5.3 w specyfikacji C # 5.0). Dlatego nie jest zdefiniowane, czy funkcja anonimowa jest statyczna, czy nie. Co więcej, sekcja K.6 pozostawia wiele otwartości co do szczegółów drzew ekspresji.

Peter O.
źródło
2
+1 najprawdopodobniej nie należy polegać na tym zachowaniu z podanych powodów; jest to bardzo szczegółowa implementacja.
Lukazoid
18

W Roslyn zmieniono zachowanie buforowania delegatów. Wcześniej, jak wspomniano, każde wyrażenie lambda, które nie przechwytuje zmiennych, było kompilowane do plikustatic metody w miejscu wywołania. Roslyn zmieniła to zachowanie. Teraz każda lambda, która przechwytuje zmienne lub nie, jest przekształcana w klasę wyświetlania:

Biorąc pod uwagę ten przykład:

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

Natywne wyjście kompilatora:

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

Delegowanie zmian zachowania buforowania w Roslyn wyjaśnia, dlaczego ta zmiana została wprowadzona.

Yuval Itzchakov
źródło
2
Dzięki, zastanawiałem się, dlaczego moja metoda Func <int> f = () => 5 nie była statyczna
vc 74
1

Metoda nie ma domknięć, a także odwołuje się do samej metody statycznej (Console.WriteLine), więc spodziewałbym się, że będzie statyczna. Metoda zadeklaruje otaczający typ anonimowy dla zamknięcia, ale w tym przypadku nie jest to wymagane.

Mel Padden
źródło