C# 为什么 ReSharper 告诉我“隐式捕获的闭包”?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13633617/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 09:15:49  来源:igfitidea点击:

Why does ReSharper tell me "implicitly captured closure"?

c#linqresharper

提问by PiousVenom

I have the following code:

我有以下代码:

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

Now, I've added a comment on the line that ReSharperis suggesting a change. What does it mean, or why would it need to be changed? implicitly captured closure: end, start

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

采纳答案by Console

The warning tells you that the variables endand startstay alive as any of the lambdas inside this method stay alive.

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

Take a look at the short example

看一个简短的例子

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

I get an "Implicitly captured closure: g" warning at the first lambda. It is telling me that gcannot be garbage collectedas long as the first lambda is in use.

我在第一个 lambda 处收到“隐式捕获的闭包:g”警告。它告诉我,只要第一个 lambda 正在使用中,g就不能被垃圾收集

The compiler generates a class for both lambda expressions and puts all variables in that class which are used in the lambda expressions.

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

So in my example gand iare held in the same class for execution of my delegates. If gis a heavy object with a lot of resources left behind, the garbage collector couldn't reclaim it, because the reference in this class is still alive as long as any of the lambda expressions is in use. So this is a potential memory leak, and that is the reason for the R# warning.

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

@splintor As in C# the anonymous methods are always stored in one class per method there are two ways to avoid this:

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

  1. Use an instance method instead of an anonymous one.

  2. Split the creation of the lambda expressions into two methods.

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

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

回答by Smartkid

Agreed with Peter Mortensen.

同意彼得·莫特森的意见。

The C# compiler generates only one type that encapsulates all variables for all lambda expressions in a method.

C# 编译器仅生成一种类型,该类型将所有 lambda 表达式的所有变量封装在一个方法中。

For example, given the source code:

例如,给定源代码:

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

The compiler generates a type looks like :

编译器生成的类型如下:

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

And the Capturemethod is compiled as:

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

Though the second lambda does not use x, it cannot be garbage collected as xis compiled as a property of the generated class used in the lambda.

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

回答by Jason Dufair

For Linq to Sql queries, you may get this warning. The lambda's scope may outlive the method due to the fact that the query is often actualized after the method is out of scope. Depending on your situation, you may want to actualize the results (i.e. via .ToList()) within the method to allow for GC on the method's instance vars captured in the L2S lambda.

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

回答by Drew Noakes

The warning is valid and displayed in methods that have more than one lambda, and they capture different values.

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

When a method that contains lambdas is invoked, a compiler-generated object is instantiated with:

当一个包含 lambdas 的方法被调用时,一个编译器生成的对象被实例化:

  • instance methods representing the lambdas
  • fields representing all values captured by anyof those lambdas
  • 表示 lambda 表达式的实例方法
  • 表示任何这些 lambda 表达式捕获的所有值的字段

As an example:

举个例子:

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

Examine the generated code for this class (tidied up a little):

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

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

Note the instance of LambdaHelpercreated stores both p1and p2.

请注意LambdaHelpercreated的实例同时存储p1p2

Imagine that:

设想:

  • callable1keeps a long-lived reference to its argument, helper.Lambda1
  • callable2does not keep a reference to its argument, helper.Lambda2
  • callable1保持对其论点的长期引用, helper.Lambda1
  • callable2不保留对其论点的引用, helper.Lambda2

In this situation, the reference to helper.Lambda1also indirectly references the string in p2, and this means that the garbage collector will not be able to deallocate it. At worst it is a memory/resource leak. Alternatively it may keep object(s) alive longer than otherwise needed, which can have an impact on GC if they get promoted from gen0 to gen1.

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

回答by anatol

You could always figure out with a reasons of R# suggestions just by clicking on the hints like shown below:

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

enter image description here

在此处输入图片说明

This hint will direct you here.

这个提示将引导您到这里



This inspection draws your attention to the fact that more closure values are being captured than is obviously visibly, which has an impact on the lifetime of these values.

Consider the following code:

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

In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For the second closure, we can see that obj1 is being explicitly captured, but ReSharper is warning us that obj2 is being implicitly captured.

This is due to an implementation detail in the C# compiler. During compilation, closures are rewritten into classes with fields that hold the captured values, and methods that represent the closure itself. The C# compiler will only create one such private class per method, and if more than one closure is defined in a method, then this class will contain multiple methods, one for each closure, and it will also include all captured values from all closures.

If we look at the code that the compiler generates, it looks a little like this (some names have been cleaned up to ease reading):

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

When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn't used in one of the closures, it will still be captured. This is the "implicit" capture that ReSharper is highlighting.

The implication of this inspection is that the implicitly captured closure value will not be garbage collected until the closure itself is garbage collected. The lifetime of this value is now tied to the lifetime of a closure that does not explicitly use the value. If the closure is long lived, this might have a negative effect on your code, especially if the captured value is very large.

Note that while this is an implementation detail of the compiler, it is consistent across versions and implementations such as Microsoft (pre and post Roslyn) or Mono's compiler. The implementation must work as described in order to correctly handle multiple closures capturing a value type. For example, if multiple closures capture an int, then they must capture the same instance, which can only happen with a single shared private nested class. The side effect of this is that the lifetime of all captured values is now the maximum lifetime of any closure that captures any of the values.

此检查让您注意到这样一个事实,即捕获的闭合值比明显可见的要多,这会影响这些值的生命周期。

考虑以下代码:

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