何时可以进行 null 检查引发 NullReferenceException

When can a null check throw a NullReferenceException

提问人:The Red Fox 提问时间:1/6/2021 最后编辑:marc_sThe Red Fox 更新时间:2/22/2021 访问量:3602

问:

我知道这乍一看似乎是不可能的,一开始对我来说也是这样,但最近我看到了这种代码抛出一个,所以这绝对是可能的。NullReferenceException

不幸的是,谷歌上几乎没有任何结果可以解释何时可以抛出 NRE,这可能使调试和理解它发生的原因变得困难。因此,为了记录这种看似奇怪的事件可能发生的方式。foo == null

此代码可以通过哪些方式抛出 ?foo == nullNullReferenceException

C# nullReferenceException

评论

14赞 Joe Sewell 1/6/2021
静态类型是否实现运算符?foo==
9赞 Serg 1/6/2021
如果可以在调试器下重现异常,则只需将调试器配置为在 NullReferenceException 的第一次出现异常时停止。这将允许您查看实际抛出异常的位置(包括 get-ters、重载运算符等)。
5赞 ckuri 1/6/2021
如果您想在检查实例是否为 null 时保持安全并忽略任何运算符覆盖,您可以执行 。这与调用 .foo is nullReferenceEquals(foo, null);
4赞 Peter Duniho 1/6/2021
“这个问题主要是为了探究原因......”——Stack Overflow 不是“探究原因”的地方。这些问题过于宽泛,缺乏重点,在各个方面都不符合现场标准。事实是:你得到了一个你无法解释的异常,解释它的唯一方法是提供抛出异常的代码,而你还没有这样做。...
3赞 The Red Fox 1/7/2021
@PeterDuniho:我编辑了我的问题,希望能让我的意图更清晰。据我了解,在 SO 上询问 X 发生的所有可能方式应该是可以的,尤其是当 X 是如此令人眼花缭乱且罕见的事情发生时。同样,我已经修复了自己的代码,这与它无关。它只是受到它的激励,以及当我在谷歌上搜索它时缺乏任何关于这个主题的有用链接。我只是想让未来的人更容易调试和理解为什么他们的 null 检查会抛出 NRE。回答这样的编程问题不是很符合 SO 的精神吗?

答:

38赞 Jonesopolis 1/6/2021 #1

在 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);
    }
}

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

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

评论

14赞 Jon Skeet 1/6/2021
术语说明:这是重载 - 您不能在 C# 中重写运算符。
1赞 jrh 1/6/2021
我想补充一点,IMO 如果您不检查参数是否为,这是一个糟糕的运算符设计,我通常会将空性因素纳入等价检查,例如,如果两者都为真,则返回 true,如果一个为 true 但不是两个,则返回 false。IIRC 这也是一些类在 .NET 引用源中的做法。null==null
0赞 Kirk Woll 1/7/2021
@jrh只要确保使用,否则你会得到一个无限循环。object.ReferenceEquals
0赞 jrh 1/7/2021
@KirkWoll是的,我已经有一段时间没有这样做了,但我想我使用了这样的东西
1赞 Jon Skeet 1/25/2021
@KirkWoll:或者更好(现在更简洁、更惯用),使用 .is null
16赞 ekke 1/6/2021 #2

一个例子是 getters:

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赞 David L 1/6/2021 #3

虽然非常深奥,但可以通过自定义实现 导致此类行为。这将是一个罕见但有趣的例子,说明这种情况可能发生的地方: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赞 CodeCaster 1/6/2021 #4

不是字面上的代码,但等待空任务也会抛出:

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;
}

但请注意,调试器可能会错误地获取抛出语句的位置。它可能会显示在相等性检查中引发的异常,而它发生在早期的代码中。

评论

0赞 The Red Fox 1/6/2021
“但是请注意,调试器可能会错误地获取抛出语句的位置。它可能会显示在相等性检查中抛出的异常,而它发生在早期的代码中。我以前从未听说过。您能否解释一下这种情况是如何或何时发生的,以及是否有某种方法可以避免这种情况?
4赞 CodeCaster 1/6/2021
一个明显的原因是调试发布版本或使用过时的 PDB,以及具有多个 try-catch-throw 块的代码。另请参阅堆栈跟踪上的行号错误和堆栈跟踪中的行号错误
1赞 Alexei Levenkov 1/6/2021
请注意,null 几乎可以保证在模拟接口的单元测试中发生。也就是说,您错过了设置的匹配条件,并且您获得了任务的默认结果。可靠地多次使人感到困惑。TaskMoq<IMyInterfaceWithAsync>null
0赞 CodeCaster 1/6/2021
@Alexei Setup() 和 MockBehavior.Strict 的所有内容,除了记录器。
0赞 Joshua 1/7/2021
@TheRedFox:我看过了。分组括号后跟。要么是过时的 PDB,要么是(上一行以执行 void 函数调用结束,该函数抛出 null 和 (该函数由抖动内联或不被视为可调试代码)) 或者上一行是 throw 语句。
1赞 Joshua 1/7/2021 #5

foo == null确实对运算符重载解析,并且有问题的运算符没有处理传递 null 的情况。我们开始考虑编写过时的,而更喜欢(从 Visual Basic 中获取一个页面),或者很快将显式内联 null 指针检查。foo == nullfoo is null!(foo is null)full is not null

修复您的实现。它不应该扔,但它是。operator==

评论

2赞 41686d6564 1/8/2021
“修复你的运算符==实现”什么实现?OP 没有提到任何关于操作员超载的事情。而且,无论如何,这种可能性已经包含在琼斯波利斯的回答中。==