Esse fechamento de extensão de vida do objeto é um bug do compilador C #?

136

Eu estava respondendo a uma pergunta sobre a possibilidade de fechamentos (legitimamente) estenderem a vida útil do objeto quando deparei com um código-gen extremamente curioso da parte do compilador C # (4.0, se necessário).

A reprodução mais curta que posso encontrar é a seguinte:

  1. Crie um lambda que captura um local ao chamar um método estático do tipo que o contém.
  2. Atribua a referência de delegação gerada a um campo de instância do objeto que contém.

Resultado: o compilador cria um objeto de fechamento que faz referência ao objeto que criou o lambda, quando não há motivo para - o destino 'interno' do delegado é um método estático e os membros da instância do objeto de criação de lambda não precisam ser (e não é) tocado quando o delegado é executado. Efetivamente, o compilador está agindo como o programador capturou thissem motivo.

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

O código gerado a partir de uma versão de compilação (descompilado para C # 'mais simples') se parece com o seguinte:

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 o <>4__thiscampo do objeto de fechamento é preenchido com uma referência de objeto, mas nunca é lido (não há motivo).

Então, o que está acontecendo aqui? A especificação de idioma permite isso? Isso é um bug / estranheza do compilador ou existe um bom motivo (que estou claramente ausente) para o fechamento fazer referência ao objeto? Isso me deixa ansioso porque isso parece uma receita para programadores felizes em encerrar (como eu) introduzir involuntariamente vazamentos de memória estranhos (imagine se o delegado foi usado como manipulador de eventos) nos programas.

Ani
fonte
19
Interessante. Parece um bug para mim. Observe que se você não atribuir a um campo de instância (por exemplo, se você retornar o valor), ele não será capturado this.
Jon Skeet
15
Não consigo reproduzir isso com a visualização do VS11 Developer. Pode reproduzir novamente no VS2010SP1. Parece que é fixo :)
leppie
2
Isso também acontece no VS2008SP1. Para o VS2010SP1, isso acontece para 3.5 e 4.0.
leppie
5
Hum, bug é uma palavra muito grande para se aplicar a isso. O compilador apenas gera código ligeiramente ineficiente. Certamente não é um vazamento, esse lixo é coletado sem problemas. Provavelmente foi corrigido quando eles trabalharam na implementação assíncrona.
Hans Passant
7
@Hans, isso não seria uma coleta de lixo sem problemas se o delegado sobreviver à vida útil do objeto, e não há nada impedindo que isso aconteça.
SoftMemes

Respostas:

24

Isso com certeza parece um bug. Obrigado por chamar minha atenção. Eu vou dar uma olhada. É possível que ele já tenha sido encontrado e corrigido.

Eric Lippert
fonte
7

Parece ser um bug ou desnecessário:

Eu executo você exemplo em 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

Exemplo 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: (Note !! Agora a referência se foi!)

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
    }

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


}

em IL: (este ponteiro está de volta)

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 nos três casos, o método-b__0 () - tem a mesma aparência:

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 nos três casos, há uma referência a um método estático, tornando-o mais estranho. Então, depois dessa análise, vou dizer que é um bug / não adianta. !

Niklas
fonte
Suponho que isso significa que é uma má idéia usar métodos estáticos de uma classe pai dentro de uma expressão lambda gerada pela classe aninhada? Eu só me pergunto se Foo.InstanceMethodé feito estático, isso removeria a referência também? Eu ficaria grato por saber.
Ivaylo Slavov
1
@Ivaylo: Se Foo.InstanceMethodtambém fosse estático, não haveria instância à vista e, portanto, não havia como thiscapturar o fechamento.
Ani
1
@Ivaylo Slavov Se o método da instância for estático, o campo deve ser estático, tentei - e não haverá um 'este ponteiro'.
Niklas #
@ Niklas, obrigado. Concluindo, suponho que métodos estáticos para criar lambdas garantam a falta desse ponteiro desnecessário.
Ivaylo Slavov
@Ivaylo Slavov, acho que sim :)
Niklas