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


296

我有以下代码:

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


6
MyCodeSucks请修正已接受的答案:kevingessner的一个是错误的(如评论中所述),如果用户未注意到Console的答案,则将其标记为已接受会误导用户。
Albireo 2014年

1
如果您在try / catch之外定义列表并在try / catch中进行所有添加,然后将结果设置为另一个对象,则也可能会看到此信息。在try / catch中移动定义/添加将允许GC。希望这是有道理的。
米哈·蒙托亚

Answers:


391

该警告告诉您变量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表达式中使用的变量放入该类中。

因此,在我的示例中gi它们与我的委托在同一类中执行。如果if g是一个有大量资源的重物,则垃圾回收器无法回收它,因为只要使用了任何lambda表达式,此类中的引用仍然有效。因此这是潜在的内存泄漏,这就是发出R#警告的原因。

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

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

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


30
有什么可能的方法来避免这种捕获?
splintor

2
感谢您提供出色的答案-我了解到,即使只在一个地方使用非匿名方法也是有原因的。
ScottRhee 2014年

1
@splintor实例化委托中的对象,或将其作为参数传递。据我所知,在上述情况下,所需的行为实际上是保留对Random实例的引用。
Casey 2014年

2
@emodendroket正确,目前我们正在谈论代码样式和可读性。一个字段更容易推断。如果内存压力或对象生存期很重要,则选择该字段,否则将其保留在更简洁的闭包中。
yzorg 2014年

1
我的案例(简化)简化为工厂方法,可以创建Foo和Bar。然后,它将订阅捕获lambas的事件订阅到这两个对象暴露的事件中,令人惊讶的是,Foo使得从Bar事件的lamba中捕获的捕获保持活跃,反之亦然。我来自C ++,这种方法可以很好地工作,并且发现这里的规则有所不同,这让我有些惊讶。我猜你知道的越多。
dlf

35

同意彼得·莫滕森的观点。

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中使用的生成类的属性进行编译而对其进行垃圾回收。


31

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

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

  • 代表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创建的商店p1和的实例p2

设想:

  • callable1 对其参数保留了很长一段时间, helper.Lambda1
  • callable2 没有引用其参数, helper.Lambda2

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


如果我们p1callable2这样的引用中删除:callable2(() => { p2.ToString(); });-这是否仍不会导致LambdaHelper仍然包含p1和的相同问题(垃圾收集器将无法对其进行分配)p2
安东尼

1
是的,将会存在相同的问题。编译器LambdaHelper为父方法中的所有lambda 创建一个捕获对象(即上面的对象)。因此,即使callable2不使用p1它,它也将共享与相同的捕获对象callable1,并且该捕获对象将同时引用p1p2。请注意,这仅对引用类型真正重要,p1在此示例中为值类型。
Drew Noakes

3

对于Linq to Sql查询,您可能会收到此警告。Lambda的作用域可能会超出该方法的寿命,因为这样的事实通常是在方法超出作用域后才实现查询。根据您的情况,您可能想在方法中实现结果(即通过.ToList()),以允许在L2S lambda中捕获的方法的实例变量上进行GC。


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,则它们必须捕获同一实例,这只能在单个共享私有嵌套类中发生。这样做的副作用是,所有捕获的值的生存期现在是捕获任何值的任何闭包的最大生存期。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.