ReSharper neden bana “örtük olarak ele geçirilmiş kapanış” diyor?


298

Takip koduna sahibim:

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

Şimdi, ReSharper'ın bir değişiklik önerdiği çizgiye bir yorum ekledim . Bu ne anlama geliyor, ya da neden değiştirilmesi gerekiyor?implicitly captured closure: end, start


6
MyCodeSucks lütfen kabul edilen cevabı düzeltin: kevingessner'ın yanıtı yanlıştır (yorumlarda açıklandığı gibi) ve kabul edilmiş olarak işaretlenmesi, Konsol'un cevabını fark etmezlerse kullanıcıları yanlış yönlendirir.
Albireo

1
Listenizi bir try / catch dışında tanımlarsanız ve try / catch içindeki tüm eklemelerinizi yaparsanız ve sonuçları başka bir nesneye ayarlarsanız da bunu görebilirsiniz. Tanımlama / toplama işleminin try / catch içinde taşınması GC'ye izin verecektir. Umarım bu mantıklıdır.
Micah Montoya

Yanıtlar:


392

Uyarı, bu yöntemin içindeki lambdalardan herhangi biri olarak değişkenlerin endve startcanlı kalmanın canlı kaldığını bildirir .

Kısa örneğe bir göz atın

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

İlk lambda'da "Örtülü olarak ele geçirilmiş bir kapatma: g" uyarısı alıyorum. O bana anlatıyor gedilemez çöp toplanan sürece ilk lamda kullanımda olduğu gibi.

Derleyici hem lambda ifadeleri için bir sınıf oluşturur ve bu sınıftaki lambda ifadelerinde kullanılan tüm değişkenleri koyar.

Yani benim örneğimde gve idelegelerimin yürütülmesi için aynı sınıfta tutuluyorlar. Çok fazla gkaynağı olan ağır bir nesne varsa , çöp toplayıcı onu geri alamazdı, çünkü bu sınıftaki referans, lambda ifadelerinden herhangi biri kullanımda olduğu sürece hala hayatta. Bu potansiyel bir bellek sızıntısıdır ve R # uyarısının nedeni budur.

@ splintor Anonim yöntemler her zaman yöntem başına bir sınıfta saklanır C # gibi bu önlemek için iki yolu vardır:

  1. Anonim bir yöntem yerine bir örnek yöntemi kullanın.

  2. Lambda ifadelerinin oluşturulmasını iki yönteme ayırın.


30
Bu yakalamadan kaçınmanın olası yolları nelerdir?
kıymık

2
Bu harika cevap için teşekkürler - Anonim olmayan bir yöntem kullanmanın bir nedeni olduğunu bile öğrendim.
ScottRhee

1
@ splintor Temsilci içindeki nesneyi başlatın veya bunun yerine parametre olarak iletin. Yukarıdaki durumda, anlayabildiğim kadarıyla, istenen davranış aslında Randomörneğe bir referans tutmaktır .
Casey

2
@emodendroket Doğru, bu noktada kod stili ve okunabilirlikten bahsediyoruz. Bir alanın akıl yürütmesi daha kolaydır. Bellek baskısı veya nesne yaşamları önemliyse, alanı seçerdim, yoksa daha kısa bir kapanışta bırakardım.
yzorg

1
Benim durumum (ağır) basitleştirilmiş bir Foo ve bir Bar oluşturan bir fabrika yöntemine kaynatıldı. Daha sonra bu iki nesnenin maruz kaldığı olaylara lambas yakalama abone olur ve sürpriz, Foo Bar etkinliğinin lambasından yakalananları canlı ve tam tersi tutar. Bu yaklaşımın işe yarayacağı C ++ 'dan geliyorum ve kuralların burada farklı olduğunu bulmak için biraz şaşırdım. Ne kadar çok bilirsen, sanırım.
dlf

35

Peter Mortensen ile anlaştı.

C # derleyicisi, bir yöntemdeki tüm lambda ifadeleri için tüm değişkenleri kapsayan tek bir tür oluşturur.

Örneğin, kaynak kodu verildiğinde:

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

Derleyici aşağıdaki gibi bir tür oluşturur:

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

Ve Captureyöntem şöyle derlenir:

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

İkinci lambda kullanılmasa da, lambdada kullanılan üretilen sınıfın bir özelliği olarak derlendiği gibi xtoplanamaz x.


31

Uyarı geçerlidir ve birden fazla lambda içeren yöntemlerde görüntülenir ve farklı değerleri yakalarlar .

Lambdas içeren bir yöntem çağrıldığında, derleyici tarafından oluşturulan bir nesne aşağıdakilerle başlatılır:

  • lambdaları temsil eden örnek yöntemler
  • bu lambdalardan herhangi biri tarafından yakalanan tüm değerleri temsil eden alanlar

Örnek olarak:

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

Bu sınıf için oluşturulan kodu inceleyin (biraz düzenlenmiş):

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

LambdaHelperOluşturulan mağazaların hem p1ve hem de örneğine dikkat edin p2.

Şunu hayal edin:

  • callable1 argümanına uzun ömürlü bir referans tutar, helper.Lambda1
  • callable2 argümanına atıfta bulunmaz, helper.Lambda2

Bu durumda, helper.Lambda1dolaylı olarak dizeye başvuruda bulunulur p2ve bu da çöp toplayıcının onu yeniden konumlandıramayacağı anlamına gelir. En kötüsü bir bellek / kaynak sızıntısıdır. Alternatif olarak, nesne (ler) i gerekenden daha uzun süre canlı tutabilir, bu da gen0'dan gen1'e yükseltildikleri takdirde GC üzerinde bir etkisi olabilir.


biz başvuru çıkarsak p1gelen callable2böyle: callable2(() => { p2.ToString(); });- bu hala aynı sorunu (çöp toplayıcı bunu ayırması mümkün olmayacaktır) neden olmaz LambdaHelperhala içerecektir p1ve p2?
Antony

1
Evet, aynı sorun var olacaktı. Derleyici LambdaHelper, üst yöntemdeki tüm lambda'lar için bir yakalama nesnesi (yani yukarıda) oluşturur. Bu nedenle callable2, hiç kullanılmasa bile p1, aynı yakalama nesnesini paylaşır callable1ve bu yakalama nesnesi hem p1ve hem de başvurur p2. Bunun yalnızca referans türleri için gerçekten önemli olduğunu ve p1bu örnekte bir değer türü olduğunu unutmayın.
Drew Noakes

3

Linq - Sql sorguları için bu uyarıyı alabilirsiniz. Lambda'nın kapsamı, yöntemin kapsam dışında kaldıktan sonra sorgunun sıklıkla gerçekleştirilmesi nedeniyle yöntemin dışında kalabilir. Durumunuza bağlı olarak, yöntemin L2S lambda'da yakalanan değişkenleri üzerinde GC'ye izin vermek için yöntem içindeki sonuçları (yani .ToList () aracılığıyla) gerçekleştirmek isteyebilirsiniz.


2

Her zaman sadece aşağıda gösterilen ipuçlarını tıklayarak R # önerilerinin nedenlerini anlayabilirsiniz:

resim açıklamasını buraya girin

Bu ipucu sizi buraya yönlendirecektir .


Bu inceleme, dikkatinizi açıkça görülebilenden daha fazla kapatma değerinin yakalandığına dikkat çeker ve bu da bu değerlerin kullanım ömrünü etkiler.

Aşağıdaki kodu göz önünde bulundurun:

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

İlk kapanışta, hem obj1 hem de obj2'nin açıkça yakalandığını görüyoruz; bunu sadece koda bakarak görebiliriz. İkinci kapanış için obj1'in açıkça yakalandığını görebiliriz, ancak ReSharper bize obj2'nin örtük olarak yakalandığını bildiriyor.

Bunun nedeni C # derleyicisindeki bir uygulama ayrıntısıdır. Derleme sırasında, kapanışlar, yakalanan değerleri tutan alanlarla ve kapamanın kendisini temsil eden yöntemlerle sınıflara yeniden yazılır. C # derleyicisi, yöntem başına yalnızca bir tane özel sınıf oluşturur ve bir yöntemde birden çok kapatma tanımlanırsa, bu sınıf, her bir kapatma için bir tane olmak üzere birden çok yöntem içerir ve ayrıca tüm kapanmalardan elde edilen tüm değerleri içerir.

Derleyicinin oluşturduğu koda bakarsak, buna biraz benziyor (okuma kolaylaştırmak için bazı isimler temizlendi):

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

Yöntem çalıştığında, tüm kapaklar için tüm değerleri yakalayan display sınıfını oluşturur. Bu nedenle, kapaklardan birinde bir değer kullanılmasa bile, yine de yakalanır. Bu, ReSharper'ın vurguladığı "örtük" yakalamadır.

Bu incelemenin sonucu, örtük olarak yakalanan kapatma değerinin, kapağın kendisi çöp toplanana kadar toplanmayacağıdır. Bu değerin ömrü, değeri açıkça kullanmayan bir kapağın ömrüne bağlanmıştır. Kapatma uzun ömürlü ise, özellikle yakalanan değer çok büyükse, kodunuz üzerinde olumsuz bir etkisi olabilir.

Bu, derleyicinin bir uygulama ayrıntısı olsa da, Microsoft (Roslyn öncesi ve sonrası) veya Mono'nun derleyicisi gibi sürümler ve uygulamalar arasında tutarlı olduğunu unutmayın. Değer türünü yakalayan birden çok kapağı doğru bir şekilde işlemek için uygulamanın açıklandığı gibi çalışması gerekir. Örneğin, birden çok kapatma bir int yakalarsa, aynı örneği yakalamaları gerekir; bu, yalnızca tek bir paylaşılan özel iç içe sınıfla gerçekleşebilir. Bunun yan etkisi, yakalanan tüm değerlerin ömrünün, artık değerlerden herhangi birini yakalayan herhangi bir kapağın maksimum ömrü olmasıdır.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.