我正在调查一位同事在通过 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
。因此,它会愉快地接受 other/obj 的 null 值并按预期返回 false。
该类型既不会覆盖==
运算符也不会覆盖运算!=
符。
Equals(Identity other)
即便如此,如果异常是从我的类的实现中抛出的,我希望在堆栈跟踪顶部看到我的Identity
类,但它说NullReferenceException
来自mscorlib
.
我在 .NET Framework 版本 4.0.30319.269 上运行。
我没有内存转储,我以前没有见过这个,从那以后也没有复制过。尽管如此,我还是有义务进行调查并绝对确定它不是由我们的代码引起的,并且不会在生产中发生。
所以,真正的问题是:是什么导致了这个异常?
- mscorlib 中的错误(似乎极不可能)
- 机器上的瞬时内存损坏(可能,难以用证据备份)
- 其他?
*响应Jordão的更新*
是否可以使用不是身份的对象调用该方法?
的ConcurrentDictionary<TKey, TValue>
类型是TKey
=Identity
并且没有任何子类Identity
。所以,我看不出这怎么可能。
是否可以使用 null 调用该方法?
单元测试涵盖了Equals
使用 null 调用所有实现的场景。
堆栈跟踪来自哪个版本的代码?也许一些旧版本容易受到异常影响?
我正在分析生成异常的相同代码。我检查了我同事计算机上运行的 .NET Framework 的版本也是 4.0.30319.269。
任何多线程场景都可能导致异常?这些通常很难重现,但可能值得研究。
是的,代码是多线程的,并且是有意的。所以,这就是我使用ConcurrentDictionary
.
* 与 Jalal Aldeen Saa'd 的回应相关的后续行动 *
我原以为只有在使用“ref”关键字通过引用传递参数时,才会导致其他一些线程设置x
为的竞争条件。我开始使用以下代码验证该理论:null
x
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) then the test fails with a
NullReferenceException , but this does not match the method signature of
GenericEqualityComparer.Equals(T x, T y)`),我可以强制执行该行为。