我正在调查一些奇怪的对象生命周期问题,发现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
。然而,编译器生成了一个单一的闭包实现类<>c__DisplayClass1_0
,其中包含hashSet
和file
。因此,返回的CreateStream
委托包含并保持对hashSet
对象的引用,而该对象应该在TestMethod
返回后可供GC。
在我遇到这个问题的实际情况中,有一个非常大的(即>100mb)对象被错误地封闭。
我的具体问题是:
- 这是一个bug吗?如果不是,为什么这种行为被认为是可取的?
更新:
C# 5规范7.15.5.1说:
当匿名函数引用外部变量时,外部变量被捕获了。通常,局部变量的生命周期仅限于与其关联的块或语句的执行(§5.1.7)。但是,被捕获的外部变量的生命周期至少延长到从匿名函数创建的委托或表达式树成为垃圾回收的候选对象。
这似乎在某种程度上是开放解释的,并且并未明确禁止lambda捕获它不引用的变量。但是,这个问题涵盖了相关场景,Eric Lippert认为它是一个bug。我个人认为,编译器提供的组合闭包实现是一种很好的优化,但是这种优化不应该用于编译器可以合理检测到可能具有超出当前堆栈帧寿命的lambda。
- 我怎样才能避免放弃使用lambda而对抗这种情况?特别是我怎样才能以防御性的方式编写代码,以便未来的代码更改不会突然导致同一方法中的某个其他未更改的lambda开始封闭它不应该封闭的内容?
更新:
我提供的代码示例必须是人为构造的。显然,将lambda创建重构为单独的方法可以解决这个问题。我的问题不是关于设计最佳实践(由@peter-duniho很好地覆盖)。相反,鉴于TestMethod
中的内容,我想知道是否有任何方法可以强制编译器排除createStream
lambda在组合闭包实现之外。
记录一下,我正在使用.NET 4.6和VS 2015。