302

我有以下代码:

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

4

5 回答 5

392

警告告诉您变量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就不能进行垃圾收集。

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

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

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

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

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

于 2013-04-05T20:49:17.167 回答
36

同意彼得莫滕森的观点。

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 中使用的生成类的属性。

于 2014-10-31T10:58:47.763 回答
31

警告有效并显示在具有多个 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++; });
    }
}

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

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 产生影响。

于 2015-06-24T10:12:18.647 回答
3

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

于 2015-05-22T00:39:05.487 回答
2

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

在此处输入图像描述

这个提示将引导你到这里


此检查会引起您的注意,即捕获的闭包值比明显可见的要多,这会影响这些值的生命周期。

考虑以下代码:

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

在第一个闭包中,我们看到 obj1 和 obj2 都被显式捕获;我们可以通过查看代码看到这一点。对于第二个闭包,我们可以看到 obj1 被显式捕获,但 ReSharper 警告我们 obj2 被隐式捕获。

这是由于 C# 编译器中的实现细节造成的。在编译期间,闭包被重写为具有保存捕获值的字段和表示闭包本身的方法的类。C# 编译器只会为每个方法创建一个这样的私有类,如果在一个方法中定义了多个闭包,那么这个类将包含多个方法,每个闭包一个,并且它还将包含来自所有闭包的所有捕获值。

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

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

当该方法运行时,它会为所有闭包创建捕获所有值的显示类。因此,即使某个值没有在其中一个闭包中使用,它仍然会被捕获。这是 ReSharper 强调的“隐式”捕获。

此检查的含义是隐式捕获的闭包值将不会被垃圾收集,直到闭包本身被垃圾收集。该值的生命周期现在与未显式使用该值的闭包的生命周期相关联。如果闭包长期存在,这可能会对您的代码产生负面影响,尤其是在捕获的值非常大的情况下。

请注意,虽然这是编译器的实现细节,但它在不同版本和实现之间是一致的,例如 Microsoft(Roslyn 前后)或 Mono 的编译器。为了正确处理捕获值类型的多个闭包,实现必须按描述工作。例如,如果多个闭包捕获一个 int,那么它们必须捕获同一个实例,这只能发生在单个共享私有嵌套类中。这样做的副作用是所有捕获值的生命周期现在是捕获任何值的任何闭包的最大生命周期。

于 2018-08-29T14:44:43.010 回答