Questo oggetto-life-extending-closing è un bug del compilatore C #?


136

Stavo rispondendo a una domanda sulla possibilità di chiusure (legittimamente) estendendo la durata degli oggetti quando ho incontrato un code-gen estremamente curioso da parte del compilatore C # (4.0 se è importante).

La riproduzione più breve che riesco a trovare è la seguente:

  1. Crea un lambda che acquisisce un locale mentre chiama un metodo statico del tipo contenente.
  2. Assegnare il riferimento delegato generato a un campo di istanza dell'oggetto contenitore.

Risultato: il compilatore crea un oggetto di chiusura che fa riferimento all'oggetto che ha creato la lambda, quando non ha motivo di: la destinazione "interna" del delegato è un metodo statico e i membri dell'istanza della lambda-creando-oggetto non devono essere (e non essere) toccato quando viene eseguito il delegato. In effetti, il compilatore si comporta come se il programmatore avesse catturato thissenza motivo.

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

Il codice generato da una build di rilascio (decompilato in C # "più semplice") è simile al seguente:

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

Osservare che il <>4__thiscampo dell'oggetto di chiusura è popolato con un riferimento a un oggetto ma non viene mai letto (non c'è motivo).

Quindi cosa sta succedendo qui? La specifica della lingua lo consente? Si tratta di un bug del compilatore / stranezza o c'è una buona ragione (che mi manca chiaramente) per la chiusura per fare riferimento all'oggetto? Questo mi rende ansioso perché sembra una ricetta per programmatori felici della chiusura (come me) per introdurre inconsapevolmente strane perdite di memoria (immagina se il delegato fosse usato come gestore di eventi) nei programmi.


19
Interessante. A me sembra un bug. Si noti che se non si assegna a un campo di istanza (ad es. Se si restituisce il valore), non viene acquisito this.
Jon Skeet,

15
Non posso riproporlo con l'anteprima dello sviluppatore VS11. Può riprodurre in VS2010SP1. Sembra che sia stato risolto :)
leppie il

2
Questo succede anche in VS2008SP1. Per VS2010SP1, succede sia per 3.5 che per 4.0.
Leppie,

5
Hmm, bug è una parola terribilmente grande da applicare a questo. Il compilatore genera semplicemente un codice leggermente inefficiente. Certamente non è una perdita, questa immondizia si raccoglie senza problemi. Probabilmente è stato risolto quando hanno lavorato all'implementazione asincrona.
Hans Passant,

7
@Hans, questo non verrebbe raccolto senza problemi se il delegato sopravvivesse alla vita dell'oggetto e non c'è nulla che impedisca che ciò accada.
SoftMemes,

Risposte:


24

Sembra sicuramente un bug. Grazie per averlo portato alla mia attenzione. Ci penserò. È possibile che sia già stato trovato e corretto.


7

Sembra essere un bug o non necessario:

Ti faccio un esempio in lingua inglese:

.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

Esempio 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: (Nota !! ora questo riferimento è sparito!)

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
    }

Esempio 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: (Questo puntatore è tornato)

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

E in tutti e tre i casi il metodo-b__0 () - ha lo stesso aspetto:

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
    }

E in tutti e 3 i casi c'è un riferimento a un metodo statico, quindi lo rende più strano. Quindi, dopo questa piccola analisi, dirò che è un bug / per niente di buono. !


Suppongo che questo significhi che è MALE usare metodi statici da una classe genitrice all'interno di un'espressione lambda generata dalla classe nidificata? Mi chiedo solo se Foo.InstanceMethodviene reso statico, questo eliminerebbe anche il riferimento? Sarei grato di saperlo.
Ivaylo Slavov,

1
@Ivaylo: se Foo.InstanceMethodfossero anche statici, non ci sarebbero istanze in vista, e quindi nessun modo per thisessere catturati dalla chiusura.
Ani,

1
@Ivaylo Slavov Se il metodo dell'istanza fosse statico, allora il campo deve essere statico, ci ho provato - e non ci sarà un 'questo puntatore'.
Niklas,

@Niklas, grazie. In conclusione, suppongo che i metodi statici per la creazione di lambda garantiranno la mancanza di questo puntatore inutile.
Ivaylo Slavov il

@Ivaylo Slavov, indovina così .. :)
Niklas
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.