303 votes

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

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 suggère un changement. Qu'est-ce que cela signifie, ou pourquoi faudrait-il le changer ? implicitly captured closure: end, start

6 votes

MyCodeSucks s'il vous plaît, corrigez la réponse acceptée : celle de kevingessner est fausse (comme expliqué dans les commentaires) et l'avoir marquée comme acceptée va induire les utilisateurs en erreur s'ils ne remarquent pas la réponse de Console.

1 votes

Vous pouvez également rencontrer ce problème si vous définissez votre liste en dehors d'un try/catch et que vous effectuez tous vos ajouts dans le try/catch, puis définissez les résultats dans un autre objet. Le fait de déplacer la définition/l'ajout à l'intérieur du try/catch permettra la GC. J'espère que cela a du sens.

393voto

Console Points 1843

L'avertissement vous indique que les variables end y start reste en vie comme tous les lambdas à l'intérieur de cette méthode restent en vie.

Jetez un coup d'œil à ce court exemple

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

Je reçois un avertissement "Implicitly captured closure : g" au premier lambda. Il me dit que g ne peut être les déchets collectés tant que le premier lambda est utilisé.

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

Donc, dans mon exemple g y i sont tenus dans la même classe pour l'exécution de mes délégués. Si g est un objet lourd avec beaucoup de ressources laissées derrière lui, le ramasseur d'ordures ne pourrait pas 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 de 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.

31 votes

Quels sont les moyens possibles d'éviter cette capture ?

2 votes

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 n'est utilisée qu'à un seul endroit.

1 votes

@splintor Instancie l'objet à l'intérieur du délégué, ou le passe comme un paramètre à la place. Dans le cas ci-dessus, pour autant que je puisse dire, le comportement souhaité est en fait de maintenir une référence à l'objet Random l'instance, cependant.

57voto

kevingessner Points 7257

ReSharper vous met en garde contre un danger lié aux variables de référence et aux lambdas de C#. Lorsque vous créez la lambda que vous passez à Where() Il comprend des références à start y end de la portée de la méthode.

ReSharper ne sait pas ce qui va arriver à ce lambda après que vous l'ayez passé. Il est important de noter que si Where() stockait le lambda pour une exécution ultérieure, il stockait les références à start y end . Si vous modifiez les valeurs de ces variables avant l'exécution de la lambda, celle-ci s'exécutera avec les nouvelles valeurs, et non celles du moment de la capture. ReSharper vous avertit poliment de ce comportement involontaire possible, puisque la capture est "implicite" et peut être accidentelle.

Puisque vous ne changez pas les valeurs de start y end si ce n'est pas un problème pour vous, vous pouvez ignorer l'avertissement. Si vous deviez les modifier (par exemple si vous capturiez une variable de boucle), vous devriez créer une nouvelle référence juste pour la fermeture, qu'elle pourrait capturer en toute sécurité avec une valeur cohérente.

36voto

Smartkid Points 315

D'accord avec Peter Mortensen.

Le compilateur C# ne génère qu'un seul type qui encapsule toutes les variables de toutes les expressions lambda d'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 ressemblant à :

[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 le Capture est compilé 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 la deuxième lambda n'utilise pas x il ne peut pas être collecté comme x est compilé comme une propriété de la classe générée utilisée dans la lambda.

31voto

Drew Noakes Points 69288

L'avertissement est valable et affiché dans les méthodes qui ont plus d'un lambda et ils capturer 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
  • des champs représentant toutes les valeurs capturées par cualquier de ces lambdas

A titre d'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 (un peu nettoyé) :

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 de LambdaHelper créé stocke à la fois p1 y p2 .

Imaginez ça :

  • callable1 conserve une référence durable à son argument, helper.Lambda1
  • callable2 ne conserve pas de référence à son argument, helper.Lambda2

Dans cette situation, la référence à helper.Lambda1 fait également indirectement référence à la chaîne dans p2 ce qui signifie que le ramasseur d'ordures ne sera pas en mesure de le désallouer. Au pire, il s'agit d'une fuite de mémoire/ressource. Il se peut aussi que l'objet soit maintenu en vie plus longtemps que nécessaire, ce qui peut avoir un impact sur le GC s'il est promu de gen0 à gen1.

3voto

Jason Dufair Points 447

Pour les requêtes Linq to Sql, vous pouvez obtenir cet avertissement. La portée du lambda peut dépasser celle de la méthode, car la requête est souvent actualisée après que la méthode soit sortie de sa portée. En fonction de votre situation, vous pouvez actualiser les résultats (c'est-à-dire via .ToList()) dans la méthode pour permettre la GC sur les variables d'instance de la méthode capturées dans la lambda L2S.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X