Ist dieses Schließen der Objektlebensdauer ein C # -Compilerfehler?

136

Ich beantwortete eine Frage zur Möglichkeit von Schließungen (zu Recht), die die Lebensdauer von Objekten verlängern, als ich auf einen äußerst merkwürdigen Code-Gen des C # -Compilers stieß (4.0, wenn das wichtig ist).

Der kürzeste Repro, den ich finden kann, ist der folgende:

  1. Erstellen Sie ein Lambda, das ein lokales erfasst, während Sie eine statische Methode des enthaltenen Typs aufrufen .
  2. Weisen Sie die generierte Delegatenreferenz einem Instanzfeld des enthaltenen Objekts zu.

Ergebnis: Der Compiler erstellt ein Abschlussobjekt, das auf das Objekt verweist, das das Lambda erstellt hat, wenn es keinen Grund dazu hat. Das 'innere' Ziel des Delegaten ist eine statische Methode, und die Instanzmitglieder des Lambda-Erstellungsobjekts müssen dies nicht berührt werden (und nicht), wenn der Delegat ausgeführt wird. Tatsächlich verhält sich der Compiler so, wie der Programmierer ihn thisohne Grund erfasst hat .

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

Der generierte Code aus einem Release-Build (dekompiliert in 'einfacheres' C #) sieht folgendermaßen aus:

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

Beachten Sie, dass das <>4__thisFeld des Abschlussobjekts mit einer Objektreferenz gefüllt ist, aber niemals gelesen wird (es gibt keinen Grund).

Also, was ist hier los? Erlaubt die Sprachspezifikation dies? Ist dies ein Compiler-Fehler / eine Kuriosität oder gibt es einen guten Grund (den ich eindeutig vermisse), dass der Abschluss auf das Objekt verweist? Dies macht mich ängstlich, da dies wie ein Rezept für abschlussfreudige Programmierer (wie mich) aussieht, unabsichtlich seltsame Speicherlecks (stellen Sie sich vor, der Delegierte würde als Event-Handler verwendet) in Programme einzuführen.

Ani
quelle
19
Interessant. Sieht für mich wie ein Käfer aus. Beachten Sie, dass wenn Sie keinem Instanzfeld zuweisen (z. B. wenn Sie den Wert zurückgeben), es nicht erfasst wird this.
Jon Skeet
15
Ich kann dies mit der VS11 Developer-Vorschau nicht wiederholen. Kann in VS2010SP1 repro. Scheint, es ist behoben :)
Leppie
2
Dies geschieht auch in VS2008SP1. Bei VS2010SP1 geschieht dies sowohl für 3.5 als auch für 4.0.
Leppie
5
Hmm, Bug ist ein schrecklich großes Wort dafür. Der Compiler generiert nur leicht ineffizienten Code. Mit Sicherheit kein Leck, dieser Müll sammelt sich problemlos. Es wurde wahrscheinlich behoben, als sie an der asynchronen Implementierung arbeiteten.
Hans Passant
7
@Hans, dies würde keine Müllabfuhr ohne Probleme bedeuten, wenn der Delegat die Lebensdauer des Objekts überleben würde, und nichts hindert dies daran.
SoftMemes

Antworten:

24

Das sieht sicher nach einem Bug aus. Vielen Dank, dass Sie mich darauf aufmerksam gemacht haben. Ich werde es untersuchen. Möglicherweise wurde es bereits gefunden und behoben.

Eric Lippert
quelle
7

Es scheint ein Fehler oder unnötig zu sein:

Ich führe dich beispielhaft in 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

Beispiel 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: (Hinweis !! jetzt ist diese Referenz weg!)

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
    }

Beispiel 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) { }
    }


}

in IL: (Dieser Zeiger ist zurück)

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

Und in allen drei Fällen sieht die Methode-b__0 () gleich aus:

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
    }

In allen drei Fällen wird auf eine statische Methode verwiesen, wodurch sie merkwürdiger wird. Also nach dieser kleinen Analyse werde ich sagen, es ist ein Fehler / für nichts Gutes. !

Niklas
quelle
Ich nehme an, dies bedeutet, dass es eine SCHLECHTE Idee ist, statische Methoden aus einer übergeordneten Klasse in einem Lambda-Ausdruck zu verwenden, der von der verschachtelten Klasse generiert wird. Ich frage mich nur, ob dies Foo.InstanceMethodstatisch gemacht wird. Würde dies auch die Referenz entfernen? Ich wäre dankbar zu wissen.
Ivaylo Slavov
1
@Ivaylo: Wenn Foo.InstanceMethodes auch statisch wäre, wäre keine Instanz in Sicht und daher keine Möglichkeit, irgendeine Art von thisSchließung zu erfassen.
Ani
1
@Ivaylo Slavov Wenn die Instanzmethode statisch war, muss das Feld statisch sein, ich habe es versucht - und es wird keinen 'this-Zeiger' geben.
Niklas
@ Niklas, danke. Zusammenfassend nehme ich an, dass statische Methoden zum Erstellen von Lambdas das Fehlen dieses unnötigen Zeigers garantieren.
Ivaylo Slavov
@ Ivaylo Slavov, denke mal .. :)
Niklas