为什么ReSharper告诉我“隐式捕获关闭”?

时间:2012-11-29 19:53:37

标签: c# linq resharper

我有以下代码:

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

现在,我在ReSharper建议更改的行中添加了评论。这是什么意思,或者为什么需要改变? implicitly captured closure: end, start

5 个答案:

答案 0 :(得分:383)

警告告诉您变量endstart保持活动状态,因为此方法中的任何lambda都保持活动状态。

看一下简短的例子

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

我在第一个lambda得到一个“隐式捕获的闭包:g”警告。它告诉我,只要第一个lambda正在使用,g就不能garbage collected

编译器为两个lambda表达式生成一个类,并将所有变量放在lambda表达式中使用的该类中。

因此在我的示例中gi被保存在同一个类中以执行我的委托。如果g是一个有大量资源的重型对象,则垃圾收集器无法回收它,因为只要任何lambda表达式正在使用,此类中的引用仍然存在。所以这是潜在的内存泄漏,这就是R#警告的原因。

@splintor 与在C#中一样,匿名方法总是存储在每个方法的一个类中,有两种方法可以避免这种情况:

  1. 使用实例方法而不是匿名方法。

  2. 将lambda表达式的创建拆分为两种方法。

答案 1 :(得分:31)

同意Peter Mortensen的观点。

C#编译器只生成一个类型,它封装了方法中所有lambda表达式的所有变量。

例如,给定源代码:

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

编译器生成的类型如下:

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

Capture方法编译为:

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

虽然第二个lambda不使用x,但它不能被垃圾收集,因为x被编译为lambda中使用的生成类的属性。

答案 2 :(得分:28)

警告有效并显示在多个lambda 的方法中,并且捕获不同的值

当调用包含lambdas的方法时,编译器生成的对象将实例化为:

  • 表示lambdas的实例方法
  • 字段,表示由任何的lambdas
  • 捕获的所有值

举个例子:

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

检查此类的生成代码(整理一下):

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

请注意,LambdaHelper创建的实例会同时存储p1p2

想象一下:

  • callable1保留对其参数helper.Lambda1
  • 的长期引用
  • callable2没有引用其参数helper.Lambda2

在这种情况下,对helper.Lambda1的引用也间接引用p2中的字符串,这意味着垃圾收集器将无法释放它。在最坏的情况下,它是内存/资源泄漏。或者,它可以使对象保持活动的时间长于其他需要的时间,如果它们从gen0升级到gen1,则会对GC产生影响。

答案 3 :(得分:3)

对于Linq to Sql查询,您可能会收到此警告。由于查询通常在方法超出范围后实现,因此lambda的范围可能比方法更长。根据您的具体情况,您可能希望在方法中实现结果(即通过.ToList()),以允许在L2S lambda中捕获的方法实例变量上使用GC。

答案 4 :(得分:2)

您总是可以通过单击如下所示的提示来找出R#建议的原因:

enter image description here

此提示将指导您here


  

此检查使您注意到以下事实:更多封闭   所捕获的值显然不是显而易见的,它具有   影响这些值的寿命。

     

考虑以下代码:

     

使用系统;公共课程Class1 {       私人动作_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);
    };
} } In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For
     

第二个闭包,我们可以看到obj1被显式捕获,   但是ReSharper向我们警告obj2被隐式捕获。

     

这是由于C#编译器中的实现细节。中   编译时,将闭包重写为具有以下字段的类:   捕获的值以及代表闭包本身的方法。   C#编译器只会为每个方法创建一个此类私有类,   如果在一个方法中定义了多个闭包,则该类   将包含多个方法,每个方法一个,并且它还将   包括所有闭包中捕获的所有值。

     

如果我们看一下编译器生成的代码,看起来会有点   像这样(为了方便阅读,一些名称已被清理):

     

公共类Class1 {       [编译器生成]       私有密封类<> c__DisplayClass1_0       {           公共对象obj1;           公共对象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);
} } When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn't used
     

在其中一个闭包中,仍将被捕获。这是   ReSharper突出显示的“隐式”捕获。

     

此检查的含义是隐式捕获   关闭值直到关闭本身才会被垃圾回收   被垃圾收集。现在,此值的生命周期已与   没有显式使用该值的闭包的生存期。如果   闭包是长期存在的,这可能会对您的代码产生负面影响,   尤其是当捕获的值很大时。

     

请注意,尽管这是编译器的实现细节,但它   在各个版本和实现(例如Microsoft)之间保持一致   (在Roslyn之前和之后)或Mono的编译器。实施必须工作   如所述,以便正确处理多个闭包捕获   值类型。例如,如果多个闭包捕获一个int,则   他们必须捕获相同的实例,只有在   单个共享的私有嵌套类。这样做的副作用是   现在,所有捕获值的生存期是任何   捕获任何值的闭包。