Pourquoi ReSharper me dit-il «fermeture implicitement capturée»?

296

J'ai le code suivant:

public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
    Log("Calculating Daily Pull Force Max...");

    var pullForceList = start == null
                             ? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
                             : _pullForce.Where(
                                 (t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 && 
                                           DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();

    _pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);

    return _pullForceDailyMax;
}

Maintenant, j'ai ajouté un commentaire sur la ligne que ReSharper propose un changement. Qu'est-ce que cela signifie ou pourquoi devrait-il être modifié?implicitly captured closure: end, start

PiousVenom
la source
6
MyCodeSucks veuillez corriger la réponse acceptée: celle de kevingessner est erronée (comme expliqué dans les commentaires) et la faire marquer comme acceptée induira les utilisateurs en erreur s'ils ne remarquent pas la réponse de la console.
Albireo
1
Vous pouvez également voir ceci si vous définissez votre liste en dehors d'un try / catch et faites tout votre ajout dans le try / catch, puis définissez les résultats sur un autre objet. Déplacer la définition / l'ajout dans le try / catch permettra GC. J'espère que cela a du sens.
Micah Montoya

Réponses:

391

L'avertissement vous indique que les variables endet startrestent en vie comme n'importe quel lambda à l'intérieur de cette méthode restent en vie.

Jetez un oeil à l'exemple court

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    int i = 0;
    Random g = new Random();
    this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
    this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}

J'obtiens un avertissement "Fermeture implicitement capturée: g" au premier lambda. Cela me dit que gles déchets ne peuvent pas être ramassés tant que le premier lambda est utilisé.

Le compilateur génère une classe pour les deux expressions lambda et place toutes les variables dans cette classe qui sont utilisées dans les expressions lambda.

Donc, dans mon exemple get isont détenus dans la même classe pour l'exécution de mes délégués. S'il gs'agit d'un objet lourd avec beaucoup de ressources, le garbage collector n'a pas pu le récupérer, car la référence dans cette classe est toujours vivante tant que l'une des expressions lambda est utilisée. Il s'agit donc d'une fuite de mémoire potentielle, et c'est la raison de l'avertissement R #.

@splintor Comme en C # les méthodes anonymes sont toujours stockées dans une classe par méthode, il y a deux façons d'éviter cela:

  1. Utilisez une méthode d'instance au lieu d'une méthode anonyme.

  2. Divisez la création des expressions lambda en deux méthodes.

Console
la source
30
Quels sont les moyens possibles d'éviter cette capture?
Splintor
2
Merci pour cette excellente réponse - j'ai appris qu'il y a une raison d'utiliser une méthode non anonyme même si elle est utilisée à un seul endroit.
ScottRhee
1
@splintor Instanciez l'objet à l'intérieur du délégué, ou passez-le plutôt comme paramètre. Dans le cas ci-dessus, pour autant que je sache, le comportement souhaité consiste en fait à contenir une référence à l' Randominstance.
Casey
2
@emodendroket Correct, à ce stade, nous parlons de style de code et de lisibilité. Un champ est plus facile à raisonner. Si la pression de la mémoire ou la durée de vie des objets sont importantes, j'ai choisi le champ, sinon je le laisserais dans la fermeture la plus concise.
yzorg
1
Mon cas (très) simplifié se résumait à une méthode d'usine qui crée un Foo et un Bar. Il souscrit ensuite la capture de lambas aux événements exposés par ces deux objets et, surprise surprise, le Foo maintient en vie les captures du lamba de l'événement Bar et vice-versa. Je viens du C ++ où cette approche aurait très bien fonctionné, et j'ai été plus qu'étonné de constater que les règles étaient différentes ici. Plus vous en savez, je suppose.
dlf
35

D'accord avec Peter Mortensen.

Le compilateur C # génère un seul type qui encapsule toutes les variables pour toutes les expressions lambda dans une méthode.

Par exemple, étant donné le code source:

public class ValueStore
{
    public Object GetValue()
    {
        return 1;
    }

    public void SetValue(Object obj)
    {
    }
}

public class ImplicitCaptureClosure
{
    public void Captured()
    {
        var x = new object();

        ValueStore store = new ValueStore();
        Action action = () => store.SetValue(x);
        Func<Object> f = () => store.GetValue();    //Implicitly capture closure: x
    }
}

Le compilateur génère un type qui ressemble à:

[CompilerGenerated]
private sealed class c__DisplayClass2
{
  public object x;
  public ValueStore store;

  public c__DisplayClass2()
  {
    base.ctor();
  }

  //Represents the first lambda expression: () => store.SetValue(x)
  public void Capturedb__0()
  {
    this.store.SetValue(this.x);
  }

  //Represents the second lambda expression: () => store.GetValue()
  public object Capturedb__1()
  {
    return this.store.GetValue();
  }
}

Et la Captureméthode est compilée comme:

public void Captured()
{
  ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
  cDisplayClass2.x = new object();
  cDisplayClass2.store = new ValueStore();
  Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
  Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}

Bien que le second lambda n'utilise pas x, il ne peut pas être récupéré comme il xest compilé en tant que propriété de la classe générée utilisée dans le lambda.

Enfant intelligent
la source
31

L'avertissement est valide et affiché dans les méthodes qui ont plus d'un lambda , et elles capturent des valeurs différentes .

Lorsqu'une méthode qui contient des lambdas est invoquée, un objet généré par le compilateur est instancié avec:

  • méthodes d'instance représentant les lambdas
  • champs représentant toutes les valeurs capturées par l' un de ces lambdas

Par exemple:

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var p1 = 1;
        var p2 = "hello";

        callable1(() => p1++);    // WARNING: Implicitly captured closure: p2

        callable2(() => { p2.ToString(); p1++; });
    }
}

Examinez le code généré pour cette classe (rangé un peu):

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var helper = new LambdaHelper();

        helper.p1 = 1;
        helper.p2 = "hello";

        callable1(helper.Lambda1);
        callable2(helper.Lambda2);
    }

    [CompilerGenerated]
    private sealed class LambdaHelper
    {
        public int p1;
        public string p2;

        public void Lambda1() { ++p1; }

        public void Lambda2() { p2.ToString(); ++p1; }
    }
}

Notez l'instance des LambdaHelpermagasins créés à la fois p1etp2 .

Imagine ça:

  • callable1 garde une référence de longue date à son argument, helper.Lambda1
  • callable2 ne garde pas de référence à son argument, helper.Lambda2

Dans cette situation, la référence à fait helper.Lambda1également référence indirectement à la chaîne dans p2, ce qui signifie que le garbage collector ne pourra pas la désallouer. Au pire, c'est une fuite de mémoire / ressource. Alternativement, il peut garder les objets en vie plus longtemps que nécessaire, ce qui peut avoir un impact sur GC s'ils sont promus de gen0 à gen1.

Drew Noakes
la source
si nous avons la référence p1de la callable2manière suivante: callable2(() => { p2.ToString(); });- serait - ce pas encore faire la même question (garbage collector ne sera pas en mesure de le désallouer) comme LambdaHelperva encore contenir p1et p2?
Antony
1
Oui, le même problème existerait. Le compilateur crée un objet de capture (c'est LambdaHelper-à- dire ci - dessus) pour tous les lambdas dans la méthode parent. Ainsi, même s'il callable2n'était pas utilisé p1, il partagerait le même objet de capture que callable1, et cet objet de capture ferait référence à la fois à p1et p2. Notez que cela n'a vraiment d'importance que pour les types de référence, et p1dans cet exemple, il s'agit d'un type de valeur.
Drew Noakes
3

Pour les requêtes Linq à Sql, vous pouvez obtenir cet avertissement. La portée de lambda peut survivre à la méthode car la requête est souvent actualisée une fois que la méthode est hors de portée. Selon votre situation, vous souhaiterez peut-être actualiser les résultats (c'est-à-dire via .ToList ()) dans la méthode pour permettre le GC sur les variables d'instance de la méthode capturées dans le lambda L2S.

Jason Dufair
la source
2

Vous pouvez toujours comprendre avec une raison de suggestions R # simplement en cliquant sur les conseils comme indiqué ci-dessous:

entrez la description de l'image ici

Cet indice vous dirigera ici .


Cette inspection attire votre attention sur le fait que plus de valeurs de fermeture sont capturées que ce n'est visiblement, ce qui a un impact sur la durée de vie de ces valeurs.

Considérez le code suivant:

using System; 
public class Class1 {
    private Action _someAction;

    public void Method() {
        var obj1 = new object();
        var obj2 = new object();

        _someAction += () => {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        };

        // "Implicitly captured closure: obj2"
        _someAction += () => {
            Console.WriteLine(obj1);
        };
    }
}

Dans la première fermeture, nous voyons que obj1 et obj2 sont explicitement capturés; nous pouvons le voir simplement en regardant le code. Pour la deuxième fermeture, nous pouvons voir que obj1 est explicitement capturé, mais ReSharper nous avertit que obj2 est implicitement capturé.

Cela est dû à un détail d'implémentation dans le compilateur C #. Pendant la compilation, les fermetures sont réécrites en classes avec des champs qui contiennent les valeurs capturées et des méthodes qui représentent la fermeture elle-même. Le compilateur C # ne créera qu'une seule classe privée par méthode, et si plus d'une fermeture est définie dans une méthode, cette classe contiendra plusieurs méthodes, une pour chaque fermeture, et elle inclura également toutes les valeurs capturées de toutes les fermetures.

Si nous regardons le code que le compilateur génère, il ressemble un peu à ceci (certains noms ont été nettoyés pour faciliter la lecture):

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object obj2;

        internal void <Method>b__0()
        {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        }

        internal void <Method>b__1()
        {
            Console.WriteLine(obj1);
        }
    }

    private Action _someAction;

    public void Method()
    {
        // Create the display class - just one class for both closures
        var dc = new Class1.<>c__DisplayClass1_0();

        // Capture the closure values as fields on the display class
        dc.obj1 = new object();
        dc.obj2 = new object();

        // Add the display class methods as closure values
        _someAction += new Action(dc.<Method>b__0);
        _someAction += new Action(dc.<Method>b__1);
    }
}

Lorsque la méthode s'exécute, elle crée la classe d'affichage, qui capture toutes les valeurs, pour toutes les fermetures. Ainsi, même si une valeur n'est pas utilisée dans l'une des fermetures, elle sera toujours capturée. Il s'agit de la capture "implicite" que ReSharper met en évidence.

L'implication de cette inspection est que la valeur de fermeture implicitement capturée ne sera pas récupérée jusqu'à ce que la fermeture elle-même soit récupérée. La durée de vie de cette valeur est désormais liée à la durée de vie d'une fermeture qui n'utilise pas explicitement la valeur. Si la fermeture dure longtemps, cela pourrait avoir un effet négatif sur votre code, surtout si la valeur capturée est très grande.

Notez que bien qu'il s'agisse d'un détail d'implémentation du compilateur, il est cohérent entre les versions et implémentations telles que Microsoft (avant et après Roslyn) ou le compilateur de Mono. L'implémentation doit fonctionner comme décrit afin de gérer correctement plusieurs fermetures capturant un type de valeur. Par exemple, si plusieurs fermetures capturent un int, elles doivent capturer la même instance, ce qui ne peut se produire qu'avec une seule classe imbriquée privée partagée. L'effet secondaire de cela est que la durée de vie de toutes les valeurs capturées est désormais la durée de vie maximale de toute fermeture qui capture l'une des valeurs.

anatol
la source