提问人:The Red Fox 提问时间:1/6/2021 最后编辑:marc_sThe Red Fox 更新时间:2/22/2021 访问量:3602
何时可以进行 null 检查引发 NullReferenceException
When can a null check throw a NullReferenceException
问:
我知道这乍一看似乎是不可能的,一开始对我来说也是这样,但最近我看到了这种代码抛出一个,所以这绝对是可能的。NullReferenceException
不幸的是,谷歌上几乎没有任何结果可以解释何时可以抛出 NRE,这可能使调试和理解它发生的原因变得困难。因此,为了记录这种看似奇怪的事件可能发生的方式。foo == null
此代码可以通过哪些方式抛出 ?foo == null
NullReferenceException
答:
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 几乎可以保证在模拟接口的单元测试中发生。也就是说,您错过了设置的匹配条件,并且您获得了任务的默认结果。可靠地多次使人感到困惑。Task
Moq<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 == null
foo is null
!(foo is null)
full is not null
修复您的实现。它不应该扔,但它是。operator==
评论
foo
==
foo is null
ReferenceEquals(foo, null);