Dlaczego ReSharper mówi mi „niejawnie uchwycone zamknięcie”?

296

Mam następujący kod:

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

Teraz dodałem komentarz do linii, że ReSharper sugeruje zmianę. Co to znaczy lub dlaczego trzeba go zmienić?implicitly captured closure: end, start

PiousVenom
źródło
6
MyCodeSucks napraw poprawioną odpowiedź: odpowiedź kevingessnera jest niepoprawna (jak wyjaśniono w komentarzach), a oznaczenie jej jako zaakceptowanej wprowadzi użytkowników w błąd, jeśli nie zauważą odpowiedzi konsoli.
Albireo,
1
Możesz to również zobaczyć, jeśli zdefiniujesz swoją listę poza try / catch i dokonasz dodawania w try / catch, a następnie ustawisz wyniki na inny obiekt. Przeniesienie definicji / dodania w ramach try / catch pozwoli GC. Mam nadzieję, że ma to sens.
Micah Montoya

Odpowiedzi:

391

Ostrzeżenie mówi ci, że zmienne endi startpozostają przy życiu, tak jak wszystkie lambdas w tej metodzie pozostają przy życiu.

Spójrz na krótki przykład

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

Dostaję ostrzeżenie „Ukryte schwytanie: g” przy pierwszej lambdzie. Mówi mi, że gnie można zbierać śmieci, dopóki używana jest pierwsza lambda.

Kompilator generuje klasę dla obu wyrażeń lambda i umieszcza wszystkie zmienne w tej klasie, które są używane w wyrażeniach lambda.

Więc w moim przykładzie gi isą przetrzymywani w tej samej klasie w celu egzekucji moich delegatów. Jeśli gjest to ciężki obiekt z dużą ilością zasobów, śmiecinik nie może go odzyskać, ponieważ odwołanie w tej klasie jest wciąż aktywne, dopóki używane jest dowolne wyrażenie lambda. Jest to więc potencjalny wyciek pamięci i to jest przyczyną ostrzeżenia R #.

@splintor Podobnie jak w języku C # anonimowe metody są zawsze przechowywane w jednej klasie na metodę, można tego uniknąć na dwa sposoby:

  1. Użyj metody instancji zamiast anonimowej.

  2. Podziel tworzenie wyrażeń lambda na dwie metody.

Konsola
źródło
30
Jakie są możliwe sposoby uniknięcia tego przechwytywania?
drzazga
2
Dzięki za tę świetną odpowiedź - dowiedziałem się, że istnieje powód, aby używać metody nieanonimowej, nawet jeśli jest używana tylko w jednym miejscu.
ScottRhee
1
@splintor Utwórz obiekt w delegacie lub przekaż go jako parametr. W powyższym przypadku, o ile mogę stwierdzić, pożądanym zachowaniem jest jednak fakt posiadania odniesienia do Randominstancji.
Casey,
2
@emodendroket Prawidłowo, w tym momencie mówimy o stylu i czytelności kodu. Pole jest łatwiejsze do uzasadnienia. Jeśli presja pamięci lub czas życia obiektów są ważne, wybrałbym pole, w przeciwnym razie pozostawiłbym je w bardziej zwięzłym zamknięciu.
yzorg
1
Mój przypadek (mocno) uproszczony sprowadził się do metody fabrycznej, która tworzy Foo i Bar. Następnie subskrybuje chwytanie jagniąt wydarzeń wystawionych na działanie tych dwóch obiektów i, co zaskakujące, Foo utrzymuje obrazy z lamby z Baru przy życiu i vice versa. Pochodzę z C ++, gdzie to podejście sprawdziłoby się dobrze i byłem więcej niż trochę zaskoczony, że reguły były tutaj inne. Im więcej wiesz, tak myślę.
dlf
35

Uzgodniony z Peterem Mortensenem.

Kompilator C # generuje tylko jeden typ, który zawiera wszystkie zmienne dla wszystkich wyrażeń lambda w metodzie.

Na przykład biorąc pod uwagę kod źródłowy:

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

Kompilator generuje typ wyglądający jak:

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

A Capturemetoda jest kompilowany jako:

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

Chociaż druga lambda nie używa x , nie można jej zbierać, ponieważ xjest ona kompilowana jako właściwość wygenerowanej klasy używanej w lambda.

Bystre dziecko
źródło
31

Ostrzeżenie jest ważne i wyświetlane w metodach, które mają więcej niż jedną lambda , i przechwytują różne wartości .

Po wywołaniu metody zawierającej lambdas tworzony jest instancja kompilowanego obiektu:

  • metody instancji reprezentujące lambdas
  • pola reprezentujące wszystkie wartości uchwycone przez którąkolwiek z tych lambd

Jako przykład:

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

Sprawdź wygenerowany kod dla tej klasy (trochę uporządkowany):

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

Zwróć uwagę na wystąpienie LambdaHelperutworzonych sklepów zarówno p1ip2 .

Wyobraź sobie, że:

  • callable1 zachowuje długotrwałe odniesienie do swojego argumentu, helper.Lambda1
  • callable2 nie przechowuje odniesienia do swojego argumentu, helper.Lambda2

W tej sytuacji odwołanie do helper.Lambda1pośrednio odwołuje się również do ciągu p2, a to oznacza, że ​​śmieciarz nie będzie mógł go cofnąć. W najgorszym przypadku jest to wyciek pamięci / zasobów. Alternatywnie, może utrzymywać żywe obiekty dłużej niż jest to potrzebne, co może mieć wpływ na GC, jeśli zostaną awansowane z gen0 na gen1.

Drew Noakes
źródło
gdybyśmy wzięli odniesienie p1z callable2następującego: callable2(() => { p2.ToString(); });- czy to nadal nie spowodowałoby tego samego problemu (śmieciarz nie będzie w stanie cofnąć przydziału), co LambdaHelpernadal będzie zawierać p1i p2?
Antony
1
Tak, istniałby ten sam problem. Kompilator tworzy jeden obiekt przechwytywania (tj. LambdaHelperPowyżej) dla wszystkich lambd w metodzie nadrzędnej. Więc nawet jeśli callable2nie zostanie wykorzystany p1, będzie współdzielić ten sam obiekt przechwytywania co callable1, a ten obiekt przechwytywania będzie odwoływał się zarówno do, jak p1i do p2. Zauważ, że to naprawdę ma znaczenie tylko dla typów referencyjnych, p1aw tym przykładzie jest typem wartości.
Drew Noakes,
3

W przypadku zapytań Linq do Sql możesz otrzymać to ostrzeżenie. Zasięg lambda może przeżyć metodę, ponieważ zapytanie jest często aktualizowane po tym, jak metoda jest poza zakresem. W zależności od twojej sytuacji, możesz chcieć zaktualizować wyniki (tj. Poprzez .ToList ()) w ramach metody, aby umożliwić GC w zmiennych instancji metody zarejestrowanych w lambda L2S.

Jason Dufair
źródło
2

Zawsze możesz dowiedzieć się z powodów sugestii R #, po prostu klikając na wskazówki, jak pokazano poniżej:

wprowadź opis zdjęcia tutaj

Ta wskazówka skieruje cię tutaj .


Inspekcja ta zwraca uwagę na fakt, że rejestrowanych jest więcej wartości zamknięcia, niż jest to oczywiście widoczne, co ma wpływ na czas życia tych wartości.

Rozważ następujący kod:

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

W pierwszym zamknięciu widzimy, że zarówno obj1, jak i obj2 są jawnie rejestrowane; możemy to zobaczyć po prostu patrząc na kod. Po drugim zamknięciu możemy zobaczyć, że obj1 jest jawnie przechwytywany, ale ReSharper ostrzega nas, że obj2 jest przechwytywany w sposób dorozumiany.

Wynika to ze szczegółów implementacji w kompilatorze C #. Podczas kompilacji zamknięcia są przepisywane na klasy z polami, które przechowują przechwycone wartości, oraz metodami reprezentującymi samo zamknięcie. Kompilator C # utworzy tylko jedną taką klasę prywatną dla każdej metody, a jeśli w metodzie zdefiniowano więcej niż jedno zamknięcie, wówczas klasa ta będzie zawierać wiele metod, po jednej dla każdego zamknięcia, a także będzie zawierać wszystkie przechwycone wartości ze wszystkich zamknięć.

Jeśli spojrzymy na kod generowany przez kompilator, wygląda to trochę tak (niektóre nazwy zostały wyczyszczone, aby ułatwić czytanie):

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

Po uruchomieniu metody tworzy klasę wyświetlania, która przechwytuje wszystkie wartości dla wszystkich zamknięć. Więc nawet jeśli wartość nie zostanie użyta w jednym z zamknięć, nadal zostanie przechwycona. Jest to „niejawne” zdjęcie, które ReSharper podkreśla.

Implikacja tej kontroli polega na tym, że niejawnie uchwycona wartość zamknięcia nie zostanie wyrzucona, dopóki samo zamknięcie nie zostanie wyrzucone. Czas życia tej wartości jest teraz powiązany z czasem życia zamknięcia, które nie używa wyraźnie tej wartości. Jeśli zamknięcie jest długotrwałe, może to mieć negatywny wpływ na kod, szczególnie jeśli przechwycona wartość jest bardzo duża.

Pamiętaj, że chociaż jest to szczegół implementacji kompilatora, jest on spójny we wszystkich wersjach i implementacjach, takich jak Microsoft (przed Roslyn i po nim) lub kompilator Mono. Implementacja musi działać zgodnie z opisem, aby poprawnie obsługiwać wiele zamknięć przechwytujących typ wartości. Na przykład, jeśli wiele zamknięć przechwytuje liczbę całkowitą, muszą przechwycić tę samą instancję, co może się zdarzyć tylko w przypadku jednej wspólnej prywatnej zagnieżdżonej klasy. Efektem ubocznym jest to, że czas życia wszystkich przechwyconych wartości jest teraz maksymalnym czasem życia każdego zamknięcia, które przechwytuje dowolne wartości.

anatol
źródło