Cette fermeture d'extension de durée de vie d'objet est-elle un bogue du compilateur C #?

136

Je répondais à une question sur la possibilité que des fermetures prolongent (légitimement) la durée de vie des objets lorsque je suis tombé sur un code-gen extrêmement curieux de la part du compilateur C # (4.0 si cela compte).

Le repro le plus court que je puisse trouver est le suivant:

  1. Créez un lambda qui capture un local tout en appelant une méthode statique du type conteneur.
  2. Attribuez la référence de délégué générée à un champ d' instance de l'objet conteneur.

Résultat: le compilateur crée un objet de fermeture qui fait référence à l'objet qui a créé le lambda, quand il n'a aucune raison de le faire - la cible `` interne '' du délégué est une méthode statique , et les membres de l'instance de l'objet de création lambda n'ont pas besoin être (et ne pas) touché lorsque le délégué est exécuté. En fait, le compilateur agit comme le programmeur a capturé thissans raison.

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

Le code généré à partir d'une version de version (décompilé en C # `` plus simple '') ressemble à ceci:

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

Observez que le <>4__thischamp de l'objet de fermeture est rempli avec une référence d'objet mais n'est jamais lu (il n'y a aucune raison).

Alors qu'est-ce qui se passe ici? La spécification de la langue le permet-elle? Est-ce un bogue / bizarrerie du compilateur ou y a-t-il une bonne raison (qui me manque clairement) pour que la fermeture fasse référence à l'objet? Cela me rend anxieux parce que cela ressemble à une recette pour les programmeurs heureux de la fermeture (comme moi) d'introduire involontairement d'étranges fuites de mémoire (imaginez si le délégué était utilisé comme gestionnaire d'événements) dans les programmes.

Ani
la source
19
Intéressant. Cela me semble être un bug. Notez que si vous n'affectez pas à un champ d'instance (par exemple si vous renvoyez la valeur), il ne capture pasthis .
Jon Skeet
15
Je ne peux pas reproduire cela avec l'aperçu du développeur VS11. Peut reproduire dans VS2010SP1. Semble que c'est corrigé :)
leppie
2
Cela se produit également dans VS2008SP1. Pour VS2010SP1, cela se produit à la fois pour 3.5 et 4.0.
leppie
5
Hmm, bug est un très gros mot à appliquer à cela. Le compilateur génère simplement du code légèrement inefficace. Certainement pas une fuite, cette poubelle se ramasse sans problème. Cela a probablement été corrigé lorsqu'ils ont travaillé sur l'implémentation asynchrone.
Hans Passant
7
@Hans, cela ne ferait pas de garbage collection sans problème si le délégué survivait à la durée de vie de l'objet, et rien n'empêche que cela se produise.
SoftMemes

Réponses:

24

Cela ressemble à un bug. Merci de l'avoir porté à mon attention. Je vais l'examiner. Il est possible qu'il ait déjà été trouvé et corrigé.

Eric Lippert
la source
7

Cela semble être un bug ou inutile:

Je lance votre exemple en langage IL:

.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

Exemple 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: (Remarque !! maintenant cette référence a disparu!)

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
    }

Exemple 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: (Ce pointeur est de retour)

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

Et dans les trois cas, la méthode-b__0 () - a la même apparence:

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
    }

Et dans les 3 cas, il y a une référence à une méthode statique, donc cela la rend plus étrange. Donc, après cette petite analyse, je dirai que c'est un bug / pour rien de bon. !

Niklas
la source
Je suppose que cela signifie que c'est une mauvaise idée d'utiliser des méthodes statiques d'une classe parente à l'intérieur d'une expression lambda générée par la classe imbriquée? Je me demande simplement si Foo.InstanceMethodest rendu statique, cela supprimerait-il également la référence? Je serais reconnaissant de savoir.
Ivaylo Slavov
1
@Ivaylo: Si Foo.InstanceMethodétaient également statiques, il n'y aurait aucune instance en vue, et donc aucun moyen de thiscapturer quelque sorte que ce soit par la fermeture.
Ani
1
@Ivaylo Slavov Si la méthode d'instance était statique, alors le champ doit être statique, j'ai essayé - et il n'y aura pas de «ce pointeur».
Niklas
@Niklas, merci. En conclusion, je suppose que les méthodes statiques de création de lambdas garantiront l'absence de ce pointeur inutile.
Ivaylo Slavov
@Ivaylo Slavov, Guess so .. :)
Niklas