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

我有以下代码:

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

71738 次浏览

警告告诉你变量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被保存在同一个类中,用于执行委托。如果g是一个遗留了大量资源的沉重对象,垃圾收集器就无法回收它,因为只要有任何lambda表达式在使用,该类中的引用就仍然是活的。所以这是一个潜在的内存泄漏,这就是r#警告的原因。

< p > @splintor 在c#中,匿名方法总是存储在每个方法的一个类中,有两种方法可以避免这种情况
  1. 使用实例方法而不是匿名方法。

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

彼得·莫滕森也这么认为。

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

对于Linq to Sql查询,你可能会得到这个警告。lambda的作用域可能比方法的作用域更持久,因为查询通常是在方法超出作用域之后才实现的。根据您的情况,您可能希望在方法中实现结果(即通过. tolist()),以允许对L2S lambda中捕获的方法实例vars进行GC。

警告是有效的,并且显示在具有多于一个获取不同的值的方法中。

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

  • 表示lambdas的实例方法
  • 字段表示这些lambdas的任何捕获的所有值

举个例子:

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中的字符串,这意味着垃圾收集器将无法释放它。最坏的情况是内存/资源泄漏。或者,它可以使对象存活的时间比所需的时间长,如果它们从第0代提升到第1代,这可能会对GC产生影响。

你总能找到r#建议的原因,只需要点击下面的提示:

enter image description 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);
}
}

当该方法运行时,它创建了一个display类,用于捕获所有闭包的所有值。所以即使一个值没有被使用 在其中一个闭包中,它仍然会被捕获。这是

. ReSharper正在高亮显示的“隐式”捕获

此检查的含义是隐式捕获 闭包值直到闭包本身才会被垃圾收集 是垃圾收集。该值的生存期现在绑定到 未显式使用该值的闭包的生命周期。如果 闭包是长期存在的,这可能会对你的代码产生负面影响, 特别是当捕获值非常大时 注意,虽然这是编译器的实现细节,但它 是一致的版本和实现,如微软 (Roslyn之前和之后)或Mono的编译器。实现必须有效 如所述,以便正确处理多个闭包捕获 值类型。例如,如果多个闭包捕获一个int,则 它们必须捕获相同的实例,这只能发生在 单个共享私有嵌套类。这个的副作用是 所有捕获值的生存期现在都是任何值的最大生存期 捕获任意值的闭包