为啥 ReSharper 告诉我“隐式捕获的闭包”?

Posted

技术标签:

【中文标题】为啥 ReSharper 告诉我“隐式捕获的闭包”?【英文标题】: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。希望这是有道理的。 【参考方案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# 中一样,匿名方法始终存储在每个方法的一个类中,有两种方法可以避免这种情况:

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

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

【讨论】:

以上是关于为啥 ReSharper 告诉我“隐式捕获的闭包”?的主要内容,如果未能解决你的问题,请参考以下文章

为啥当我单击 Ctrl+Shift+N 时 resharper 无法打开文件?

告诉resharper一个Func 永远不会返回null

为啥我在 ReSharper 中收到错误“无法解析符号 <symbolname>”?

为啥我的resharper控件安装之后没有显示

为啥代码覆盖率在 ReSharper 中不起作用?

为啥 ReSharper GetHashCode 覆盖使用“397”?