【问题标题】:When can a null check throw a NullReferenceException空检查何时可以抛出 NullReferenceException
【发布时间】:2021-04-11 11:26:51
【问题描述】:

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

不幸的是,Google 上几乎没有任何结果可以解释像 foo == null 这样的代码何时会引发 NRE,这可能会导致难以调试和理解它发生的原因。因此,为了记录这种看似奇怪的事件发生的可能方式。

这段代码foo == null 会以什么方式抛出NullReferenceException

【问题讨论】:

  • foo的静态类型是否实现==操作符?
  • 如果您可以在调试器下重现异常,您只需将调试器配置为在第一次出现 NullReferenceException 异常时停止。这将允许您查看实际抛出异常的位置(包括 get-ter、重载运算符等)。
  • 如果您想在检查实例是否为空时确保安全并忽略任何运算符覆盖,您可以执行foo is null。这与调用ReferenceEquals(foo, null); 相同。
  • “这个问题主要是为了探究原因……” -- Stack Overflow 不是“探究原因”的地方。此类问题过于宽泛,缺乏重点,在各种方面都达不到现场标准。事实是:您遇到了一个无法解释的异常,而解释它的唯一方法是提供引发异常的代码,您还没有这样做。 ...
  • @PeterDuniho:我编辑了我的问题,希望能让我的意图更清晰。据我了解,在 SO 上询问 X 可能发生的所有可能方式应该是可以的,尤其是当 X 是一件如此奇怪和罕见的事情时。同样,我已经修复了我自己的代码,这与它无关。当我用谷歌搜索它时,它只是被它和缺乏关于这个主题的任何有用链接所激发。我只是想让未来的人们更容易调试并理解为什么他们的空检查会抛出 NRE。回答这样的编程问题不是很符合 SO 的精神吗?

标签: c# null nullreferenceexception


【解决方案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);
    }
}

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

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

【讨论】:

  • 术语说明:这是重载 - 您不能在 C# 中覆盖运算符。
  • 我想补充一点,如果您不检查参数是否为null,IMO 这是一个糟糕的运算符设计,我通常会在等效性检查中考虑空值,例如,在== 中,如果两者都是null,则返回true,如果一个为true 但不是两者,则返回false。 IIRC 这也是一些类在 .NET 参考源中的做法。
  • @jrh 只需确保使用object.ReferenceEquals 否则您将陷入无限循环。
  • @KirkWoll 是的,我已经有一段时间没有这样做了,但我想我使用了类似this
  • @KirkWoll:或者更好(现在更简洁、更惯用),使用is null
【解决方案2】:

一个例子是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。

【讨论】:

    【解决方案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();
        }
    }
    

    【讨论】:

      【解决方案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;
      }
      

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

      【讨论】:

      • "但是请注意,调试器可能会错误地获取抛出语句的位置。它可能会显示在相等检查时抛出的异常,而它发生在早期代码中。"我以前从未听说过。你能解释一下这种情况是如何发生的,何时发生,是否有办法避免这种情况?
      • 一个明显的原因是调试发布版本或使用过时的 PDB,但代码也包含多个 try-catch-throw 块。另见Wrong line number on stack traceWrong line numbers in stack trace
      • 请注意,空值Task 几乎可以保证在模拟接口的单元测试中发生。 IE。您错过了Moq&lt;IMyInterfaceWithAsync&gt; 中设置的匹配条件,并且您获得了任务的默认null 结果。可靠地使人们多次困惑。
      • @Alexei Setup() 所有的东西和 MockBehavior.Strict 一路,除了记录器。
      • @TheRedFox:我见过。分组括号如下。要么是过时的 PDB,要么(前一行以执行 void 函数调用而结束,并且该函数抛出 null 并且(该函数被抖动内联或不被视为可调试代码))或前一行是抛出声明。
      【解决方案5】:

      foo == null 确实可以解决运算符重载问题,并且有问题的运算符没有处理传递给 null 的问题。我们开始考虑编写已过时的 foo == null 并更喜欢(从 Visual Basic 中获取页面)foo is null!(foo is null)(即将成为 full is not null)以显式内联空指针检查。

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

      【讨论】:

      • “修复你的operator== 实现” 什么实现? OP 没有提到任何关于重载 == 运算符的内容。此外,无论如何Jonesopolis's answer 已经涵盖了这种可能性。
      猜你喜欢
      • 2014-06-15
      • 1970-01-01
      • 1970-01-01
      • 2017-05-03
      • 2014-11-25
      • 2011-10-27
      • 1970-01-01
      • 2013-05-30
      • 1970-01-01
      相关资源
      最近更新 更多