不可能的空引用异常?

32

我正在调查一个同事在通过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,TKeyTIdentity的类型都是相同的。

该类型是一个名为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;
}

测试完成并且没有错误。

如果我更改签名以通过引用传递xy(也就是,public bool Equals<T>(ref T x, ref T y)),我可以强制出现这种行为,然后测试会失败并抛出NullReferenceException,但这与GenericEqualityComparer.Equals(T x, T y)方法的签名不匹配。


在您重写的 Equals 实现中,对 obj 进行 null 检查(并返回 false),然后查看是否仍会抛出错误。 - keyboardP
1
你是否有单元测试来检验多线程环境下的Equals方法?如果没有,我建议你添加一些。 - Mike Parkhill
没有任何单元测试可以显式地以多线程方式测试Equals,但对象是不可变的,并且仅比较在构造函数中设置的私有字段,这些字段不能为null,否则构造函数将失败。此外,错误似乎并不来自我的Equals方法,而是来自GenericEqualityComparer。 - Eamon
这个涉及到“弱引用”对象吗? - Michael Stum
没有弱引用;除非本地变量x保持弱引用活动。 - Eamon
显示剩余2条评论
2个回答

4
我会在这里提出我的假设。
堆栈让你相信这是崩溃发生的地方,但实际上它发生在其他地方。我们正在查看错误的线程。
我不知道这是否可行,但有时候老式的“printf调试”很有帮助。如果在调用TryGetValue之前打印出你要查找的值,那么你就可以看到是否遇到了null。

缺乏可重复性,阻止了类似于“printf调试”的好方法。由于调试器已经附加(应用程序当时正在通过Visual Studio运行),我猜可能是这导致它报告了一个不正确的堆栈或损坏了x变量,但对我来说,这感觉很奇怪,因为通常情况下选择不会出错,因为使用Visual Studio和.NET4的人并不只有少数,但在这一点上,我准备将其称为异常现象并继续前进。 - Eamon
@Eamon,你的意思是说你现在无法重现这个错误吗? - MPelletier
2
是的,这在原始问题中已经说明了。这是我以前从未见过的一次性错误,因此需要使用异常中给出的堆栈跟踪通过代码的静态分析进行调试。因此,我认为可以将其归类为由于某种瞬态问题(内存损坏等)而产生的异常情况,超出了我的控制范围,并继续处理更重要的事情。 - Eamon

1
我在大约两年前(不确定是在3.5还是4.0版本中,或者是否已经修复)在Equals中遇到了一个空引用异常。对于你的情况,我不清楚正在比较哪些类型,但在我的情况下,每当将MethodInfo反射对象与任何非MethodInfo对象进行比较时,就会发生这种情况...崩溃!因此,如果您正在比较反射对象,则可能是这样。如果不是,那么至少我可以证明,在某些情况下,BCL中至少有一个Equals实现可能会因无法解释的原因而抛出空引用异常,因此可能还有其他问题。即使是神圣的.NET BCL也是软件,所有软件都有漏洞。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接