【问题标题】:Impossible NullReferenceException?不可能的 NullReferenceException?
【发布时间】:2012-10-15 03:13:36
【问题描述】:

我正在调查一位同事在通过 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的类型在这个stack trace中都是同一个类型。

该类型是一个名为Identity 的自定义类型,它实现了IEquatable&lt;Identity&gt;。它是不可变的,并且不能用它在其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。

该类型既不会覆盖== 运算符,也不会覆盖!= 运算符。

即便如此,如果在我的 Identity 类中从 Equals(Identity other) 的实现中抛出异常,我希望在堆栈跟踪顶部看到我的类,但它说 NullReferenceException 来自 @ 987654341@.

我在 .NET Framework 版本 4.0.30319.269 上运行。

我没有内存转储,而且我以前从未见过它,此后也没有复制过它。尽管如此,我还是有义务进行调查并绝对确定它不是由我们的代码引起的,并且不会在生产中发生。

那么,真正的问题是:是什么导致了这个异常?

  • mscorlib 中的错误(似乎极不可能)
  • 机器上的瞬时内存损坏(可能,难以用证据备份)
  • 其他?

* 响应 Jordão 的更新 *

是否可以使用不是身份的对象调用方法?

ConcurrentDictionary&lt;TKey, TValue&gt; 是这样输入的,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&lt;T&gt;(ref T x, ref T y) then the test fails with aNullReferenceException, but this does not match the method signature ofGenericEqualityComparer.Equals(T x, T y)`,我可以强制执行该行为。

【问题讨论】:

  • 在覆盖的Equals 实现中,对obj 执行null 检查(并返回false)并查看是否仍然抛出错误。
  • 问题是这个异常只被观察过一次,我不能轻易重现它,所以我只能通过对代码的静态分析来诊断原因。
  • 既然你说它是多线程的,那么在 if 检查之后但在等号之前,是否可能 x 在其他地方被设置为 null。一个简单的检查方法是在 Equals 覆盖中添加一个 sleep 并在另一个线程中将 x 值设置为 null。
  • 您是否有在多线程环境中执行 Equals 方法的单元测试?如果没有,我会添加一些。
  • 没有任何单元测试来以多线程方式显式测试 Equals,但是对象是不可变的,并且只比较在构造函数中设置的私有字段并且不能为空,否则构造函数会失败。此外,错误似乎不是来自我的 Equals 方法,而是来自 GenericEqualityComparer。

标签: c# nullreferenceexception mscorlib


【解决方案1】:

我将在这里列出我的假设。

堆栈使您相信这是崩溃发生的地方,但它发生在其他地方。我们正在查看错误的线程。

我不知道这是否实用,但有时旧的“printf 调试”会有所帮助。如果您在调用TryGetValue 之前打印出您要查找的值怎么办?你会看到你是否打空。

【讨论】:

  • 缺乏可重复性阻止了像旧的“printf 调试”这样的事情。由于附加了调试器(当时该应用程序正在通过 Visual Studio 运行),我猜这可能以某种方式导致它报告了错误的堆栈或损坏了 x 变量,但对我来说这只是感觉不对,因为通常Select Isn't Broken 是不像只有少数人使用 Visual Studio 和 .NET4,但在这一点上,我已经准备好将其称为异常并继续前进。
  • @Eamon 您的意思是此时您无法重现错误?
  • 是的,这是在原始问题中说明的。这是我以前从未见过的一次性错误,因此需要使用异常中给出的堆栈跟踪通过代码的静态分析进行调试。因此,我认为由于某种我无法控制的暂时性问题(内存损坏等)而将其归类为异常可能是公平的,然后继续处理更重要的事情。
【解决方案2】:

大约几年前,我在 Equals 中遇到了一个空引用异常(不确定它是在 3.5 还是 4.0 中,或者它是否已修复)。我不清楚在您的情况下正在比较哪些类型,但在我的情况下,每当将泛型方法声明的 MethodInfo 反射对象与任何非 MethodInfo 对象进行比较时,都会发生这种情况...... Ka-boom!因此,如果您要比较反射对象,可能就是这样。如果您不是,那么至少我可以证明 BCL 中至少有一个 Equals 实现可以在某些情况下无缘无故地抛出空引用异常,因此很可能还有其他实现。即使是神圣的 .NET BCL 仍然是软件,所有软件都有错误。

【讨论】:

    猜你喜欢
    • 2011-01-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-06
    • 1970-01-01
    相关资源
    最近更新 更多