Czy to zamknięcie okresu istnienia obiektu jest błędem kompilatora C #?

136

Odpowiadałem na pytanie o możliwość domknięć (uzasadnionych) wydłużania czasu życia obiektów, kiedy natknąłem się na jakiś niezwykle ciekawy gen kodu ze strony kompilatora C # (4.0, jeśli to ma znaczenie).

Najkrótsza reprodukcja, jaką mogę znaleźć, jest następująca:

  1. Utwórz lambdę, która przechwytuje lokalną podczas wywoływania metody statycznej typu zawierającego.
  2. Przypisz wygenerowane odwołanie do delegata do pola wystąpienia obiektu zawierającego.

Wynik: kompilator tworzy obiekt zamknięcia, który odwołuje się do obiektu, który utworzył lambdę, gdy nie ma powodu - „wewnętrznym” celem delegata jest metoda statyczna , a elementy składowe instancji obiektu tworzącego lambdę nie muszą być (i nie są) dotykane podczas wykonywania delegata. W rzeczywistości kompilator zachowuje się tak, jak programista przechwycił thisbez powodu.

class Foo
{
    private Action _field;

    public void InstanceMethod()
    {
        var capturedVariable = Math.Pow(42, 1);

        _field = () => StaticMethod(capturedVariable);
    }

    private static void StaticMethod(double arg) { }
}

Wygenerowany kod z kompilacji wydania (zdekompilowany do „prostszego” C #) wygląda następująco:

public void InstanceMethod()
{

    <>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();

    CS$<>8__locals2.<>4__this = this; // What's this doing here?

    CS$<>8__locals2.capturedVariable = Math.Pow(42.0, 1.0);
    this._field = new Action(CS$<>8__locals2.<InstanceMethod>b__0);
}

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    // Fields
    public Foo <>4__this; // Never read, only written to.
    public double capturedVariable;

    // Methods
    public void <InstanceMethod>b__0()
    {
        Foo.StaticMethod(this.capturedVariable);
    }
}

Zauważ, że <>4__thispole obiektu zamknięcia jest wypełnione odwołaniem do obiektu, ale nigdy nie jest odczytywane (nie ma powodu).

Więc co się tutaj dzieje? Czy specyfikacja języka na to pozwala? Czy jest to błąd / dziwność kompilatora, czy też istnieje dobry powód (którego wyraźnie mi brakuje), aby zamknięcie odwoływało się do obiektu? To mnie niepokoi, ponieważ wygląda to na przepis na zadowolonych z zamknięcia programistów (takich jak ja) na nieświadome wprowadzanie dziwnych wycieków pamięci (wyobraź sobie, że delegat był używany jako program obsługi zdarzeń) do programów.

Ani
źródło
19
Ciekawy. Dla mnie wygląda na błąd. Zauważ, że jeśli nie przypiszesz do pola instancji (np. Jeśli zwrócisz wartość), nie zostanie ono przechwycone this.
Jon Skeet,
15
Nie mogę tego powtórzyć w wersji zapoznawczej VS11 Developer. Można odtworzyć w VS2010SP1. Wydaje się, że to naprawione :)
leppie
2
Dzieje się tak również w VS2008SP1. W przypadku VS2010SP1 dzieje się tak zarówno dla wersji 3.5, jak i 4.0.
leppie
5
Hmm, błąd to strasznie duże słowo, które można do tego zastosować. Kompilator po prostu generuje nieco nieefektywny kod. Na pewno nie wyciek, to śmieci zbiera się bez problemu. Prawdopodobnie zostało to naprawione, gdy pracowali nad implementacją asynchroniczną.
Hans Passant
7
@Hans, to nie zbierałoby śmieci bez problemu, gdyby delegat przeżył żywotność obiektu, a nic nie stoi na przeszkodzie, aby tak się stało.
SoftMemes

Odpowiedzi:

24

To z pewnością wygląda na błąd. Dziękuję za zwrócenie mi na to uwagi. Przyjrzę się temu. Możliwe, że został już znaleziony i naprawiony.

Eric Lippert
źródło
7

Wydaje się, że to błąd lub niepotrzebne:

Uruchomię ci przykład w języku IL lang:

.method public hidebysig 
    instance void InstanceMethod () cil managed 
{
    // Method begins at RVA 0x2074
    // Code size 63 (0x3f)
    .maxstack 4
    .locals init (
        [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'   'CS$<>8__locals2'
    )

    IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: ldarg.0
    IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Make ref to this
    IL_000d: nop
    IL_000e: ldloc.0
    IL_000f: ldc.r8 42
    IL_0018: ldc.r8 1
    IL_0021: call float64 [mscorlib]System.Math::Pow(float64, float64)
    IL_0026: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
    IL_002b: ldarg.0
    IL_002c: ldloc.0
    IL_002d: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
    IL_0033: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0038: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
    IL_003d: nop
    IL_003e: ret
} // end of method Foo::InstanceMethod

Przykład 2:

class Program
{
    static void Main(string[] args)
    {
    }


    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Foo2.StaticMethod(capturedVariable);  //Foo2

        }

        private static void StaticMethod(double arg) { }
    }

    class Foo2
    {

        internal static void StaticMethod(double arg) { }
    }


}

in cl: (Uwaga! teraz to odniesienie zniknęło!)

public hidebysig 
        instance void InstanceMethod () cil managed 
    {
        // Method begins at RVA 0x2074
        // Code size 56 (0x38)
        .maxstack 4
        .locals init (
            [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1' 'CS$<>8__locals2'
        )

        IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
        IL_0005: stloc.0
        IL_0006: nop //No this pointer
        IL_0007: ldloc.0
        IL_0008: ldc.r8 42
        IL_0011: ldc.r8 1
        IL_001a: call float64 [mscorlib]System.Math::Pow(float64, float64)
        IL_001f: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
        IL_0024: ldarg.0 //No This ref
        IL_0025: ldloc.0
        IL_0026: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
        IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
        IL_0031: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
        IL_0036: nop
        IL_0037: ret
    }

Przykład 3:

class Program
{
    static void Main(string[] args)
    {
    }

    static void Test(double arg)
    {

    }

    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Test(capturedVariable);  

        }

        private static void StaticMethod(double arg) { }
    }


}

w IL: (Ten wskaźnik powrócił)

IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Back again.

We wszystkich trzech przypadkach metoda-b__0 () - wygląda tak samo:

instance void '<InstanceMethod>b__0' () cil managed 
    {
        // Method begins at RVA 0x2066
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
                   IL_0006: call void ConsoleApplication1.Program/Foo::StaticMethod(float64) //Your example
                    IL_0006: call void ConsoleApplication1.Program/Foo2::StaticMethod(float64)//Example 2
        IL_0006: call void ConsoleApplication1.Program::Test(float64) //Example 3
        IL_000b: nop
        IL_000c: ret
    }

We wszystkich 3 przypadkach istnieje odniesienie do metody statycznej, co czyni ją bardziej dziwną. Więc po tej małej analizie powiem, że to błąd / na nic. !

Niklas
źródło
Przypuszczam, że oznacza to ZŁY pomysł, aby używać statycznych metod z klasy nadrzędnej wewnątrz wyrażenia lambda generowanego przez zagnieżdżoną klasę. Zastanawiam się tylko, czy jeśli Foo.InstanceMethodjest statyczny, czy spowoduje to również usunięcie odniesienia? Byłbym wdzięczny, gdyby wiedział.
Ivaylo Slavov
1
@Ivaylo: Gdyby Foo.InstanceMethodbyły również statyczne, nie byłoby widać żadnej instancji, a zatem nie thismożna by jej uchwycić przez zamknięcie.
Ani,
1
@Ivaylo Slavov Jeśli metoda instancji była statyczna, to pole musi być statyczne, próbowałem - i nie będzie „tego wskaźnika”.
Niklas,
@Niklas, dziękuję. Podsumowując, przypuszczam, że statyczne metody tworzenia lambd zagwarantują brak tego zbędnego wskaźnika.
Ivaylo Slavov
@Ivaylo Slavov, Chyba tak .. :)
Niklas