【发布时间】:2016-02-26 22:48:03
【问题描述】:
我正在调查一些奇怪的对象生命周期问题,并遇到了 C# 编译器的这种非常令人费解的行为:
考虑以下测试类:
class Test
{
delegate Stream CreateStream();
CreateStream TestMethod( IEnumerable<string> data )
{
string file = "dummy.txt";
var hashSet = new HashSet<string>();
var count = data.Count( s => hashSet.Add( s ) );
CreateStream createStream = () => File.OpenRead( file );
return createStream;
}
}
编译器生成以下内容:
internal class Test
{
public Test()
{
base..ctor();
}
private Test.CreateStream TestMethod(IEnumerable<string> data)
{
Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0();
cDisplayClass10.file = "dummy.txt";
cDisplayClass10.hashSet = new HashSet<string>();
Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0)));
return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1));
}
private delegate Stream CreateStream();
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
public HashSet<string> hashSet;
public string file;
public <>c__DisplayClass1_0()
{
base..ctor();
}
internal bool <TestMethod>b__0(string s)
{
return this.hashSet.Add(s);
}
internal Stream <TestMethod>b__1()
{
return (Stream) File.OpenRead(this.file);
}
}
}
原始类包含两个 lambda:s => hashSet.Add( s ) 和 () => File.OpenRead( file )。第一个关闭局部变量hashSet,第二个关闭局部变量file。但是,编译器会生成一个包含hashSet 和file 的闭包实现类<>c__DisplayClass1_0。因此,返回的 CreateStream 委托包含并保持对 hashSet 对象的引用,一旦 TestMethod 返回,该对象应该可供 GC 使用。
在我遇到这个问题的实际场景中,一个非常大(即>100mb)的对象被错误地包围了。
我的具体问题是:
- 这是一个错误吗?如果不是,为什么认为这种行为是可取的?
更新:
C# 5 规范 7.15.5.1 说:
当一个外部变量被一个匿名函数引用时, 据说外部变量已被匿名捕获 功能。通常,局部变量的生命周期仅限于 执行与其关联的块或语句 (§5.1.7)。但是,捕获的外部变量的生命周期是 至少扩展到从创建的委托或表达式树 匿名函数有资格进行垃圾回收。
这似乎对某种程度的解释是开放的,并且没有明确禁止 lambda 捕获它未引用的变量。但是,this question 涵盖了一个相关的场景,@eric-lippert 认为这是一个错误。恕我直言,我认为编译器提供的组合闭包实现是一个很好的优化,但是这种优化不应该用于编译器可以合理检测到的可能具有超出当前堆栈帧的生命周期的 lambda。
- 如何在不完全放弃使用 lambda 的情况下对此进行编码?值得注意的是,我如何防御性地对此进行编码,以便将来的代码更改不会突然导致同一方法中的其他未更改的 lambda 开始包含不应该包含的内容?
更新:
我提供的代码示例必然是人为的。显然,将 lambda 创建重构为单独的方法可以解决该问题。我的问题不是关于设计最佳实践(@peter-duniho 也涵盖了)。相反,鉴于 TestMethod 的内容,我想知道是否有任何方法可以强制编译器从组合闭包实现中排除 createStream lambda。
为了记录,我的目标是 .NET 4.6 和 VS 2015。
【问题讨论】:
-
它们共享相同的词法范围。也许是因为这个。
-
Discrete Anonymous methods sharing a class? 的可能重复项。作为一个额外的好处,这个例子非常简单,但是不是做作的。
-
这是“隐式捕获关闭”的原因吗?我想我现在更好地理解了这个警告。我一直想知道为什么在某些情况下 lambda 会捕获与它无关的东西。
-
“优化不应该用于编译器可以合理检测到的 lambdas 可能有超出当前堆栈帧的生命周期”——这应该是什么意思?根据定义,all 闭包“具有超出当前堆栈帧的生命周期”。我不清楚这种行为是否是一种“良好的优化”(在闭包类的上下文中,为每个独立的 lambda 创建一个单独的类当然不会花费太多额外费用)。但如果它是一种优化,编译器会根据什么逻辑有条件地放弃它?
-
“鉴于 TestMethod 的内容,我想知道是否有任何方法可以强制编译器从组合闭包实现中排除 createStream lambda” i> - 您似乎在问“我可以更改此代码以避免问题,但这样做不更改代码?”。我已经提供了一个清晰的示例,说明您将如何避免您发布的示例中的问题;在任何此类示例中都可以使用类似的方法。但是任何变通方法都必然涉及改变方法;不然怎么可能?
标签: c# lambda closures .net-4.6