¿Es este objeto-vida-extensión-cierre un error del compilador de C #?

136

Estaba respondiendo una pregunta sobre la posibilidad de que los cierres (legítimamente) extendieran la vida útil de los objetos cuando me topé con un código genético extremadamente curioso por parte del compilador de C # (4.0 si eso importa).

La reproducción más corta que puedo encontrar es la siguiente:

  1. Cree una lambda que capture un local mientras llama a un método estático del tipo que lo contiene.
  2. Asigne la referencia delegada generada a un campo de instancia del objeto contenedor.

Resultado: el compilador crea un objeto de cierre que hace referencia al objeto que creó el lambda, cuando no tiene razón para hacerlo: el objetivo 'interno' del delegado es un método estático , y los miembros de la instancia del objeto de creación lambda no necesitan ser (y no) tocado cuando se ejecuta el delegado. Efectivamente, el compilador está actuando como el programador ha capturado thissin razón.

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

El código generado a partir de una compilación de lanzamiento (descompilado a C # 'más simple') se ve así:

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

Observe que el <>4__thiscampo del objeto de cierre se rellena con una referencia de objeto pero nunca se lee (no hay razón).

Entonces, ¿qué está pasando aquí? ¿La especificación del idioma lo permite? ¿Es este un error / rareza del compilador o hay una buena razón (que claramente me falta) para que el cierre haga referencia al objeto? Esto me pone ansioso porque parece una receta para que los programadores felices (como yo) introduzcan sin saberlo extrañas pérdidas de memoria (imagínese si el delegado se usara como controlador de eventos) en los programas.

Y yo
fuente
19
Interesante. Me parece un error. Tenga en cuenta que si no asigna a un campo de instancia (por ejemplo, si devuelve el valor), no se captura this.
Jon Skeet
15
No puedo reprobar esto con la vista previa del desarrollador VS11. Puede repro en VS2010SP1. Parece que está arreglado :)
leppie
2
Esto también sucede en VS2008SP1. Para VS2010SP1, ocurre tanto para 3.5 como para 4.0.
leppie
55
Hmm, error es una palabra muy grande para aplicar a esto. El compilador solo genera código ligeramente ineficiente. Ciertamente no es una fuga, esta basura se acumula sin ningún problema. Probablemente se solucionó cuando trabajaron en la implementación asíncrona.
Hans Passant
77
@ Hans, esto no recogería basura sin ningún problema si el delegado sobreviviera a la vida útil del objeto, y no hay nada que impida que esto suceda.
SoftMemes

Respuestas:

24

Eso seguro parece un error. Gracias por llamar mi atención. Lo miraré. Es posible que ya se haya encontrado y reparado.

Eric Lippert
fuente
7

Parece ser un error o innecesario:

Ejecuto su ejemplo en 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

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


}

en cl: (¡Nota! ¡ahora esta referencia se ha ido!)

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
    }

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


}

en IL: (Este puntero está de vuelta)

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

Y en los tres casos el método-b__0 () - se ve igual:

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
    }

Y en los 3 casos hay una referencia a un método estático, por lo que lo hace más extraño. Entonces, después de este pequeño análisis, diré que es un error / para nada bueno. !

Niklas
fuente
Supongo que esto significa que es una MALA idea usar métodos estáticos de una clase primaria dentro de una expresión lambda generada por la clase anidada. Me pregunto si Foo.InstanceMethodse hace estático, ¿eliminaría esto también la referencia? Estaría agradecido de saberlo.
Ivaylo Slavov
1
@Ivaylo: Si Foo.InstanceMethodtambién fuera estático, no habría ninguna instancia a la vista y, por lo tanto, no habría forma de thiscapturar ningún tipo de cierre.
Ani
1
@Ivaylo Slavov Si el método de la instancia fue estático, entonces el campo tiene que ser estático, lo intenté, y no habrá un 'este puntero'.
Niklas
@ Niklas, gracias. En conclusión, supongo que los métodos estáticos para crear lambdas garantizarán la falta de este puntero innecesario.
Ivaylo Slavov
@Ivaylo Slavov, supongo que sí .. :)
Niklas