我有以下代码:
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
答案 0 :(得分:383)
警告告诉您变量end
和start
保持活动状态,因为此方法中的任何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
就不能garbage collected。
编译器为两个lambda表达式生成一个类,并将所有变量放在lambda表达式中使用的该类中。
因此在我的示例中g
和i
被保存在同一个类中以执行我的委托。如果g
是一个有大量资源的重型对象,则垃圾收集器无法回收它,因为只要任何lambda表达式正在使用,此类中的引用仍然存在。所以这是潜在的内存泄漏,这就是R#警告的原因。
@splintor 与在C#中一样,匿名方法总是存储在每个方法的一个类中,有两种方法可以避免这种情况:
使用实例方法而不是匿名方法。
将lambda表达式的创建拆分为两种方法。
答案 1 :(得分:31)
同意Peter Mortensen的观点。
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中使用的生成类的属性。
答案 2 :(得分:28)
警告有效并显示在多个lambda 的方法中,并且捕获不同的值。
当调用包含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
创建的实例会同时存储p1
和p2
。
想象一下:
callable1
保留对其参数helper.Lambda1
callable2
没有引用其参数helper.Lambda2
在这种情况下,对helper.Lambda1
的引用也间接引用p2
中的字符串,这意味着垃圾收集器将无法释放它。在最坏的情况下,它是内存/资源泄漏。或者,它可以使对象保持活动的时间长于其他需要的时间,如果它们从gen0升级到gen1,则会对GC产生影响。
答案 3 :(得分:3)
对于Linq to Sql查询,您可能会收到此警告。由于查询通常在方法超出范围后实现,因此lambda的范围可能比方法更长。根据您的具体情况,您可能希望在方法中实现结果(即通过.ToList()),以允许在L2S lambda中捕获的方法实例变量上使用GC。
答案 4 :(得分:2)
此提示将指导您here。
此检查使您注意到以下事实:更多封闭 所捕获的值显然不是显而易见的,它具有 影响这些值的寿命。
考虑以下代码:
使用系统;公共课程Class1 { 私人动作_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
第二个闭包,我们可以看到obj1被显式捕获, 但是ReSharper向我们警告obj2被隐式捕获。
这是由于C#编译器中的实现细节。中 编译时,将闭包重写为具有以下字段的类: 捕获的值以及代表闭包本身的方法。 C#编译器只会为每个方法创建一个此类私有类, 如果在一个方法中定义了多个闭包,则该类 将包含多个方法,每个方法一个,并且它还将 包括所有闭包中捕获的所有值。
如果我们看一下编译器生成的代码,看起来会有点 像这样(为了方便阅读,一些名称已被清理):
公共类Class1 { [编译器生成] 私有密封类<> c__DisplayClass1_0 { 公共对象obj1; 公共对象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
在其中一个闭包中,仍将被捕获。这是 ReSharper突出显示的“隐式”捕获。
此检查的含义是隐式捕获 关闭值直到关闭本身才会被垃圾回收 被垃圾收集。现在,此值的生命周期已与 没有显式使用该值的闭包的生存期。如果 闭包是长期存在的,这可能会对您的代码产生负面影响, 尤其是当捕获的值很大时。
请注意,尽管这是编译器的实现细节,但它 在各个版本和实现(例如Microsoft)之间保持一致 (在Roslyn之前和之后)或Mono的编译器。实施必须工作 如所述,以便正确处理多个闭包捕获 值类型。例如,如果多个闭包捕获一个int,则 他们必须捕获相同的实例,只有在 单个共享的私有嵌套类。这样做的副作用是 现在,所有捕获值的生存期是任何 捕获任何值的闭包。