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

Pio*_*nom 291 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;
}
Run Code Online (Sandbox Code Playgroud)

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

Con*_*ole 386

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

看看简短的例子

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();
}
Run Code Online (Sandbox Code Playgroud)

我在第一个lambda处得到了一个"隐式捕获的闭包:g"警告.它告诉我,只要第一个lambda正在使用,g就不能收集垃圾.

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

所以在我的例子中g,i并且在同一个类中执行我的代理.如果g是遗留了大量资源的重型对象,则垃圾收集器无法回收它,因为只要正在使用任何lambda表达式,此类中的引用仍然存在.所以这是潜在的内存泄漏,这就是R#警告的原因.

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

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

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

  • 有什么方法可以避免这种捕获? (28认同)
  • 感谢这个伟大的答案 - 我已经了解到有一个理由使用非匿名方法,即使它只在一个地方使用. (2认同)
  • @emodendroket正确,此时我们正在谈论代码风格和可读性.字段更容易推理.如果内存压力或物体寿命很重要,我会选择该字段,否则我会将其留在更简洁的闭包中. (2认同)

Sma*_*kid 32

同意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
    }
}
Run Code Online (Sandbox Code Playgroud)

编译器生成的类型如下:

[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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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));
}
Run Code Online (Sandbox Code Playgroud)

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


Dre*_*kes 30

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

当调用包含lambdas的方法时,将使用以下代码实例化编译器生成的对象:

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

举个例子:

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++; });
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

注意LambdaHelper创建的商店的实例p1p2.

设想:

  • callable1 保持对其论点的长期参考, helper.Lambda1
  • callable2 没有引用它的论点, helper.Lambda2

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