何时进行空值检查会抛出NullReferenceException异常?

33

一开始可能看起来不可能,我最初也是这么认为的,但最近我见到过正是这种代码抛出了NullReferenceException,所以这绝对是有可能的。

不幸的是,谷歌上几乎没有解释当像foo == null这样的代码会抛出NRE的结果,这可能会使调试和理解发生故障的原因变得困难。因此,为了记录这种看似奇怪的情况可能发生的可能性。

哪些情况下,这个foo == null代码会抛出NullReferenceException呢?


14
foo 的静态类型是否实现了 == 运算符? - Joe Sewell
9
如果你能在调试器下重现异常,你可以配置调试器使其在 NullReferenceException 第一次出现时停止。这样可以让你看到实际抛出异常的位置(包括 getters、重载运算符等)。 - Serg
5
如果您想在检查实例是否为空时保险一些,并忽略任何操作符重载,可以使用foo is null。这与调用ReferenceEquals(foo, null);相同。 - ckuri
4
这个问题主要是为了探讨原因......-- Stack Overflow 不适合 "探讨原因"。这种问题过于宽泛,缺乏焦点,并且在各种方面都无法满足网站的标准。事实上:你遇到了一个无法解释的异常,唯一能解释它的方法就是提供抛出异常的代码,而你并没有这样做 - Peter Duniho
3
@PeterDuniho: 我修改了我的问题,希望让我的意图更加清晰。据我所知,在SO上询问X可能发生的所有可能方式应该是可以的,特别是当X是如此奇异和罕见的事情时。再次强调,我已经修复了自己的代码,与它无关。这只是受到它和在谷歌搜索此主题时缺乏任何有用链接的启发。我只是想使未来的人更容易调试并理解为什么他们的空检查会引发NRE。回答编程问题像这样难道不是SO的精神吗? - The Red Fox
显示剩余6条评论
5个回答

38

在 C# 中,您可以重载运算符以添加自定义逻辑来处理某些比较,像这样。例如:

class Test
{
    public string SomeProp { get; set; }
    
    public static bool operator ==(Test test1, Test test2)
    {
        return test1.SomeProp == test2.SomeProp;
    }

    public static bool operator !=(Test test1, Test test2)
    {
        return !(test1 == test2);
    }
}

那么这将会产生一个空引用异常:

Test test1 = null;
bool x = test1 == null;

14
术语说明:这是“重载”,你不能在C#中“覆盖”运算符。 - Jon Skeet
1
我想补充一点,如果你不检查参数是否为“null”,那么在我看来这是一个糟糕的操作符设计。通常我会将“null”考虑到等价性检查中,例如,在==中,如果两个都是“null”,则返回true,如果只有一个是true而不是两个,则返回false。如果我没记错的话,这也是一些类在.NET参考源代码中的实现方式。 - jrh
@jrh 确保使用 object.ReferenceEquals,否则会出现无限循环。 - Kirk Woll
@KirkWoll 是的,我已经有一段时间没有做过这个了,但我想我使用了类似于这个的东西。 - jrh
1
@KirkWoll:或者更好的方法(更简洁和更符合惯用语),使用 is null - Jon Skeet
@JonSkeet 哇,没想到可以在这种情况下使用那个。太棒了! - Kirk Woll

16

一个例子是使用getter方法:

class Program
{
    static void Main(string[] args)
    {
        new Example().Test();
    }
}

class Example
{
    private object foo
    {
        get => throw new NullReferenceException();
    }

    public void Test()
    {
        Console.WriteLine(foo == null);
    }
}

这段代码会产生一个NullReferenceException异常。


9

虽然相当深奥,但可以通过自定义的DynamicMetaObject实现此类行为。这将是一个罕见但有趣的例子:

void Main()
{
    dynamic foo = new TestDynamicMetaObjectProvider();
    object foo2 = 0;
    
    Console.WriteLine(foo == foo2);
}

public class TestDynamicMetaObjectProvider : IDynamicMetaObjectProvider
{
    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new TestMetaObject(parameter, BindingRestrictions.Empty, this);
    }
}

public class TestMetaObject : DynamicMetaObject
{
    public TestMetaObject(Expression expression, BindingRestrictions restrictions)
        : base(expression, restrictions)
    {
    }

    public TestMetaObject(Expression expression, BindingRestrictions restrictions, object value)
        : base(expression, restrictions, value)
    {
    }

    public override DynamicMetaObject BindBinaryOperation(BinaryOperationBinder binder, DynamicMetaObject arg)
    {
        // note it doesn't have to be an explicit throw.  Any improper property
        // access could bubble a NullReferenceException depending on the 
        // custom implementation.
        throw new NullReferenceException();
    }
}

7

这并不是说你的代码,但是等待一个空任务也会抛出异常:

public class Program
{
    public static async Task Main()
    {
        var s = ReadStringAsync();
        if (await s == null)
        {
            Console.WriteLine("s is null");
        }
    }

    // instead of Task.FromResult<string>(null);
    private static Task<string> ReadStringAsync() => null;
}

需要注意的是,调试器可能会错误地获取抛出语句的位置。它可能会显示异常在等式检查时抛出,而实际上它是在之前的代码中发生的。


请注意,调试器可能会错误地获取抛出语句的位置。它可能会显示异常在等式检查时被抛出,而实际上是在更早的代码中发生。我以前从未听说过这种情况。你能解释一下这种情况是如何发生的,是否有避免的方法? - The Red Fox
4
一个明显的原因是调试发布版本或使用过时的PDB文件,还有具有多个try-catch-throw块的代码。参见堆栈跟踪中的错误行号堆栈跟踪中的错误行号 - CodeCaster
1
请注意,在模拟接口的单元测试中,空的“Task”几乎是肯定会发生的。也就是说,如果您在Moq<IMyInterfaceWithAsync>中错过了设置匹配条件,则会得到默认的“null”结果。这可多次令人困惑。 - Alexei Levenkov
@Alexei Setup() 所有的东西并使用MockBehavior.Strict,除了日志记录器。 - CodeCaster
@TheRedFox:我看到了。分组括号跟随着。它可能是过时的PDB文件,或者(前一行以执行空函数调用并且该函数抛出了null(该函数由JIT编译器内联或未被视为可调试代码)结束)或前一行是一个throw语句。 - Joshua

1
foo == null确实会进行运算符重载,而所涉及的运算符无法处理传递的null。我们开始考虑写作foo == null已经过时,并且更喜欢(借鉴Visual Basic)foo is null!(foo is null),即将成为full is not null以显式地内联一个null指针检查。
修复您的operator==实现。它不应该抛出异常,但是它确实抛出了异常。

2
修复你的operator==实现。但是,原帖作者并没有提及重载==运算符的任何内容。此外,这种可能性已经在Jonesopolis的答案中得到了涵盖。 - 41686d6564 stands w. Palestine

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