为啥 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】:警告告诉您变量 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”警告。它告诉我 g
不能是 garbage collected 只要第一个 lambda 正在使用中。
编译器为两个 lambda 表达式生成一个类,并将用于 lambda 表达式的所有变量放入该类中。
因此,在我的示例中,g
和 i
被放在同一个班级中以执行我的代表。如果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
的实例同时创建了p1
和p2
的存储。
想象一下:
callable1
保留对其论点的长期引用,helper.Lambda1
callable2
不保留对其参数的引用,helper.Lambda2
在这种情况下,对helper.Lambda1
的引用也间接引用了p2
中的字符串,这意味着垃圾收集器将无法释放它。最坏的情况是内存/资源泄漏。或者,它可能使对象的存活时间比其他需要的时间长,如果它们从 gen0 提升到 gen1,这可能会对 GC 产生影响。
【讨论】:
如果我们像这样从callable2
中取出p1
的引用:callable2(() => p2.ToString(); );
- 这仍然不会导致与@相同的问题(垃圾收集器将无法释放它) 987654335@ 仍将包含 p1
和 p2
?
是的,同样的问题也会存在。编译器为父方法中的所有 lambda 创建一个捕获对象(即上面的 LambdaHelper
)。因此,即使callable2
没有使用p1
,它也会与callable1
共享相同的捕获对象,并且该捕获对象将引用p1
和p2
。请注意,这仅对引用类型很重要,并且此示例中的 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 无法打开文件?