我有以下代码:
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建议进行更改。它是什么意思,或者为什么它需要改变?隐式捕获闭包:end, start
警告告诉您变量结束和开始保持活动,因为该方法中的任何lambdas都保持活动。
看一下这个简短的例子
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就不能被垃圾收集。
编译器为两个lambda表达式生成一个类,并将lambda表达式中使用的所有变量放在该类中。
所以在我的例子中,g和i被保存在同一个类中,用于执行我的委托。如果g是一个遗留了大量资源的沉重对象,垃圾收集器就不能回收它,因为只要使用任何lambda表达式,该类中的引用就仍然是活动的。所以这是一个潜在的内存泄漏,这就是r#警告的原因。
@splintor
在c#中,匿名方法总是存储在每个方法的一个类中,有两种方法可以避免这种情况:
使用实例方法而不是匿名方法。
将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中使用的生成类的属性。
警告是有效的,并且显示在具有多个lambda的方法中,并且它们捕获不同的值。
当一个包含lambdas的方法被调用时,一个编译器生成的对象被实例化:
表示lambdas的实例方法
字段表示这些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实例同时存储p1和p2。
想象一下:
Callable1保留了对其参数helper的长期引用。λ₁
Callable2不保留对其参数helper的引用。Lambda2
在这种情况下,对helper的引用。Lambda1还间接引用了p2中的字符串,这意味着垃圾收集器将无法释放它。最坏的情况是内存/资源泄漏。或者,它可以使对象存活的时间比所需的时间长,如果它们从第0代提升到第1代,这可能会对GC产生影响。
你总能找到r#建议的原因,只需要点击下面的提示:
这个提示会指引你到这里。
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.