【问题标题】:Why does ReSharper tell me "implicitly captured closure"?为什么 ReSharper 告诉我“隐式捕获的闭包”?
【发布时间】:2012-11-17 23:44:15
【问题描述】:

我有以下代码:

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

【问题讨论】:

  • 如果您在 try/catch 之外定义列表并在 try/catch 中添加所有内容,然后将结果设置到另一个对象,您也可能会看到这一点。在 try/catch 中移动定义/添加将允许 GC。希望这是有道理的。

标签: c# linq resharper


【解决方案1】:

警告告诉您变量 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”警告。它告诉我 g 不能是 garbage collected 只要第一个 lambda 正在使用中。

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

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

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

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

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

【讨论】:

  • 有哪些方法可以避免这种捕获?
  • 感谢您的出色回答 - 我了解到即使只在一个地方使用非匿名方法也是有理由的。
  • @splintor 在委托中实例化对象,或者将其作为参数传递。在上述情况下,据我所知,所需的行为实际上是持有对 Random 实例的引用。
  • @emodendroket 正确,此时我们正在讨论代码样式和可读性。一个领域更容易推理。如果内存压力或对象生命周期很重要,我会选择该字段,否则我会将其留在更简洁的闭包中。
  • 我的案例(严重)简化为创建 Foo 和 Bar 的工厂方法。然后它将捕获lamba 订阅到这两个对象公开的事件,令人惊讶的是,Foo 使来自Bar 事件的lamba 的捕获保持活动状态,反之亦然。我来自 C++,这种方法本来可以很好地工作,并且惊讶地发现这里的规则不同。我猜你知道的越多。
【解决方案2】:

同意彼得莫滕森的观点。

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

【讨论】:

    【解决方案3】:

    警告有效并显示在具有多个 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 产生影响。

    【讨论】:

    • 如果我们像这样从callable2 中取出p1 的引用:callable2(() =&gt; { p2.ToString(); }); - 这仍然不会导致与@相同的问题(垃圾收集器将无法释放它) 987654335@ 仍将包含 p1p2?
    • 是的,同样的问题也会存在。编译器为父方法中的所有 lambda 创建一个捕获对象(即上面的 LambdaHelper)。因此,即使callable2 没有使用p1,它也会与callable1 共享相同的捕获对象,并且该捕获对象将引用p1p2。请注意,这仅对引用类型很重要,并且此示例中的 p1 是值类型。
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      此提示将引导您here


      这项检查提请您注意一个事实,即更多的关闭 正在捕获的值比明显可见的,它具有 影响这些值的生命周期。

      考虑以下代码:

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

      【讨论】:

        猜你喜欢
        • 2016-08-05
        • 2013-09-22
        • 2012-10-08
        • 2013-06-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多