我正在调查一个同事在通过Visual Studio 2010运行应用程序时遇到的异常:
System.NullReferenceException was unhandled by user code
Message=Object reference not set to an instance of an object.
Source=mscorlib
StackTrace:
at System.Collections.Generic.GenericEqualityComparer`1.Equals(T x, T y)
at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValue(TKey key, TValue& value)
at xxxxxxx.xxxxxxx.xxxxxxx.RepositoryBase`2.GetFromCache(TIdentity id)
使用.NET Reflector,我查看了GenericEqualityComparer<T>.Equals(T x, T y)
代码,但我没有发现可能导致NullReferenceException
的任何原因。
//GenericEqualityComparer<T>.Equals(T x, T y) from mscorlib 4.0.30319.269
public override bool Equals(T x, T y)
{
if (x != null)
{
return ((y != null) && x.Equals(y));
}
if (y != null)
{
return false;
}
return true;
}
在这个堆栈跟踪中,T,TKey
和TIdentity
的类型都是相同的。
该类型是一个名为Identity
的自定义类型,实现了IEquatable<Identity>
。它是不可变的,并且不能使用空值构造其用于实现Equals(Identity other)
的字段。它还像这样覆盖了Equals(object obj)
:
public override bool Equals(object obj)
{
if ((object)this == obj)
{
return true;
}
return Equals(obj as Identity);
}
public bool Equals(Identity other)
{
if ((object)this == (object)other)
{
return true;
}
if ((object)other == null)
{
return false;
}
if (!FieldA.Equals(other.FieldA))
{
return false;
}
return FieldB.Equals(other.FieldB);
}
我有一个相当详尽的单元测试集,涵盖了Equals实现。因此,它将高兴地接受其他/obj的null值,并像预期的那样返回false。
该类型既不覆盖==运算符也不覆盖!=运算符。
即使如此,如果NullReferenceException是从我的Identity类中的Equals(Identity other)实现抛出的,我仍然希望在堆栈跟踪的顶部看到我的类,但它说NullReferenceException来自mscorlib。
我正在运行.NET Framework版本4.0.30319.269。
我没有内存转储,并且我以前从未见过这种情况,也没有再次复制它。尽管如此,我有义务进行调查,并确保绝对确定它不是由我们的代码引起的,并且在生产中不会发生。
所以,真正的问题是:是什么导致了这个异常?
- mscorlib中的错误(似乎非常不可能) - 机器上的瞬时内存损坏(可能,很难用证据支持) - 其他?
*针对Jordão的更新*
是否可能使用不是Identity的对象调用该方法?
ConcurrentDictionary被键入,以便TKey = Identity,而没有任何子类化Identity。因此,我无法看到它是如何可能的。
是否可能使用null调用该方法?
单元测试涵盖了使用null调用所有Equals实现的情况。
堆栈跟踪的代码版本是什么?也许某个旧版本容易受到异常的影响?
我正在分析生成异常的相同代码。我已检查我的同事计算机上运行的.NET Framework版本也是4.0.30319.269。
任何多线程场景都可能导致异常吗?这些通常很难复制,但可能值得调查。
是的,代码是多线程的,并且旨在如此。因此,这就是为什么我正在使用ConcurrentDictionary的原因。
*针对Jalal Aldeen Saa'd的回复的后续*
我认为只有当参数x使用'ref'关键字通过引用传递时,其他某个线程将x设置为null的竞争条件才可能是原因。我使用以下代码验证了该理论:
ManualResetEvent TestForNull = new ManualResetEvent(false);
ManualResetEvent SetToNull = new ManualResetEvent(false);
[TestMethod]
public void Test()
{
var x = new object();
var y = new object();
var t = Task.Factory.StartNew(() =>
{
return Equals(x, y);
});
TestForNull.WaitOne(); //wait until x has been tested for null value
x = null;
SetToNull.Set(); //signal that x has now been set to null
var result = t.Result;
Assert.IsFalse(result);
}
public bool Equals<T>(T x, T y)
{
if (x != null)
{
TestForNull.Set(); //signal that we have determined that x was not null
SetToNull.WaitOne(); //wait for original x value to be set to null
//would fail here if setting the outer scope x to null affected
//the value of x in this scope
return ((y != null) && x.Equals(y));
}
if (y != null)
{
return false;
}
return true;
}
测试完成并且没有错误。
如果我更改签名以通过引用传递x
和y
(也就是,public bool Equals<T>(ref T x, ref T y)
),我可以强制出现这种行为,然后测试会失败并抛出NullReferenceException
,但这与GenericEqualityComparer.Equals(T x, T y)
方法的签名不匹配。
Equals
实现中,对obj
进行 null 检查(并返回 false),然后查看是否仍会抛出错误。 - keyboardP