简而言之,有时编译器或 JIT 在调用 C# 类对象的终结器方法之前就返回了。
为了避免产生非常大的问题,Visual C++ 2005 文档中提供了完整的代码,将作为一个“答案”发布,但下面是必要的部分:
下面的类具有“哈希”属性,该属性将返回内部数组的克隆副本。在其构造过程中,该数组的第一项的值为 2。在析构函数中,将该值设置为零。
重点是:如果您尝试获取“Example”的“Hash”属性,您将获得数组的干净副本,其第一个项目仍为 2,因为该对象正在被使用(因此不会被垃圾收集/终结):
public class Example
{
private int nValue;
public int N { get { return nValue; } }
// The Hash property is slower because it clones an array. When
// KeepAlive is not used, the finalizer sometimes runs before
// the Hash property value is read.
private byte[] hashValue;
public byte[] Hash { get { return (byte[])hashValue.Clone(); } }
public Example()
{
nValue = 2;
hashValue = new byte[20];
hashValue[0] = 2;
}
~Example()
{
nValue = 0;
if (hashValue != null)
{
Array.Clear(hashValue, 0, hashValue.Length);
}
}
}
但是事情并不那么简单......使用这个类的代码在一个线程中运行,当然,为了测试,该应用程序是高度多线程的:
public static void Main(string[] args)
{
Thread t = new Thread(new ThreadStart(ThreadProc));
t.Start();
t.Join();
}
private static void ThreadProc()
{
// running is a boolean which is always true until
// the user press ENTER
while (running) DoWork();
}
DoWork静态方法是出现问题的代码:
private static void DoWork()
{
Example ex = new Example();
byte[] res = ex.Hash; // [1]
// If the finalizer runs before the call to the Hash
// property completes, the hashValue array might be
// cleared before the property value is read. The
// following test detects that.
if (res[0] != 2)
{
// Oops... The finalizer of ex was launched before
// the Hash method/property completed
}
}
一旦DoWork执行1,000,000次,垃圾回收器会进行清理并试图回收"ex",因为它不再被函数中的剩余代码引用,并且这一次比"Hash"获取方法更快。因此,最终得到的是一个零长度字节数组的克隆,而不是正确的数组(其中第一个项在2处)。我猜测代码存在内联,这本质上替换了DoWork函数中标记为[1]的行,变成了类似以下的内容:
// Supposed inlined processing
byte[] res2 = ex.Hash2;
// note that after this line, "ex" could be garbage collected,
// but not res2
byte[] res = (byte[])res2.Clone();
假设Hash2是一个简单的访问器,代码如下:
// Hash2 code:
public byte[] Hash2 { get { return (byte[])hashValue; } }
所以问题是:在C#/.NET中这是否应该按照这种方式工作,或者这可以被视为编译器或JIT的错误? 编辑
请参阅Chris Brumme和Chris Lyons的博客以获取解释。 http://blogs.msdn.com/cbrumme/archive/2003/04/19/51365.aspx
http://blogs.msdn.com/clyon/archive/2004/09/21/232445.aspx 每个人的回答都很有趣,但我无法选择比其他更好的答案。所以我给了你们所有人一个+1 ...
抱歉
:-)
编辑2
我无法在Linux / Ubuntu / Mono上重现该问题,尽管在相同的条件下使用相同的代码(同时运行多个相同的可执行文件,发布模式等)。
!Example
实现托管C++代码,而不是使用析构函数~Example
(在C++/CLI中,这会创建IDisposable实现)。这是C++/CLI的一个怪癖,旨在为C++开发人员提供便利,他们期望在类超出范围或被删除时确定地调用析构函数(在托管情况下,从C#调用,在'using'语句超出范围或已Dispose)。 - Dan BryantDispose()
和Dispose(bool)
方法的逻辑编写,在C#中编写这些方法很麻烦(更不用说“在C#中正确编写”)。RAII不是C++的概念。RAII是一种模式,很少有语言能够正确实现。C#(甚至Java)都没有做到这一点。 - paercebalDispose(bool)
模式不适合一般使用;在大多数情况下,我可以将我的类标记为 sealed 并直接实现 Dispose()。 - Dan Bryantsealed
不是一个选项...这个混乱让我花了几周的时间,而且将再次花费几周的时间,因为修正后的处理程序代码太丑陋了,我不能不戴墨镜来保护我的眼睛看它...也许C#的下一个迭代版本将让我们使用~XXX/!XXX符号。毕竟,如果C++/CLI编译器能够生成正确的Finalize
和Dispose
代码,那么C#也应该能够做到... - paercebal