启用可为空引用类型时何时对参数进行空检查

43

在使用C# 8.0的可空引用类型特性编写程序中,对于函数参数还需要执行空值检查吗?

void Foo(string s, object o)
{
    if (s == null) throw new ArgumentNullException(nameof(s)); // Do I need these?
    if (o == null) throw new ArgumentNullException(nameof(o));
    ...
}

代码中没有任何一部分是公共API的一部分,因此我怀疑这些检查可能是多余的。两个参数没有标记为可为空,因此如果调用代码可能传入null,则编译器应该发出警告。


12
如果你有控制整个代码库来调用此方法,并且所有代码都使用启用了这个功能的C# 8进行编译,而且你很勤奋并修复编译器产生的所有警告/错误,那么你可以删除这些if语句。如果对这些要求中的任何一个不确定,那么应该保留这些if语句。 - Lasse V. Karlsen
2
通过已删除的答案中的评论,我清楚地看到您并不想了解编译器检查,而是询问其他人的观点。因此,我已投票将此问题关闭为基于主观看法。正如答案和那些评论所述,编译器检查不能替代if语句或合同系统,并且很容易被绕过。无论这种风险对您来说是否足够,完全取决于您自己。然而,似乎您已经知道新的编译器检查是做什么的了。 - Lasse V. Karlsen
1
我已经使用JetBrains注释和ReSharper/Rider相当长的一段时间了,它们可以帮助你确信你的代码是正确的,但它们并不能替代实际的检查,所以我两者都有。在可空引用类型出现之后,我将继续使用这两种工具,因为我编写的代码可能会被编译时没有进行这些检查的代码所使用,或者那些代码的编写者甚至可以直接欺骗编译器。(当我觉得需要时,我已知会采用一些小技巧)。 - Lasse V. Karlsen
3
编译器无法对任何调用代码进行文字警告--因为有逃生口和覆盖方式(如null-forgiving操作符)会阻止其分析。无论您是否使用可为空引用类型,添加这些检查应该基本上是独立的--如果您已经确信不需要它们,可以继续不使用;如果您没有这种信心,那么最好留下它们。新的检查减少了引入错误的可能性。它们是否使其足够不可能再不需要它们将取决于情况。 - Jeroen Mostert
2
@LasseVågsætherKarlsen 这是一种明确鼓励的基于专家经验的观点问题。这就是“许多好问题会根据专家经验产生某种程度的意见”。目前,许多人知道该功能的描述,但很少有人了解实际编译器限制。甚至更少的人有将大型项目迁移到使用可空引用的经验。我可以列出3个人,其中一个已经回答了。另外两个在SO工作。 - Panagiotis Kanavos
还要考虑用一组通用的守卫条款替换此类低级检查,可以自己编写或使用像 https://www.nuget.org/packages/Ardalis.GuardClauses 这样的库。 - ssmith
1个回答

38
给定一个使用C# 8.0的可空引用类型功能的程序中的函数,我是否仍应对参数执行空检查?这取决于您对API所有路径的确定程度。请考虑以下代码:
public void Foo(string x)
{
    FooImpl(x);
}

private void FooImpl(string x)
{
    ...
}

在这里,FooImpl不是公共API的一部分,但如果Foo不验证它的参数,则仍然可以接收null引用。(实际上,它可能依赖于Foo执行参数验证。)

FooImpl中进行检查当然不是多余的,因为它在执行时执行的检查是编译器无法确保的。可空引用类型提高了代码的通用安全性,更重要的是提高了代码的表现力,但它们与CLR提供的类型安全不同(例如,防止您将string引用视为Type引用)。编译器对其是否可以为null执行特定表达式的观点存在各种方式的“错误”,并且编译器可以被!覆盖。

更广泛地说:如果您的检查在C# 8之前不是多余的,那么在C# 8之后它们也不是多余的,因为可空参考类型功能除属性外不会改变代码生成的IL。

因此,如果您的公共API正在执行所有适当的参数检查(如上面的Foo),则代码中的检查已经是多余的。您对此有多么有信心?如果您非常有信心,而且出错的影响很小,那么当然可以删除验证。 C# 8功能可能会帮助您增加对此的信心,但仍需要小心,不要变得过于自信-毕竟,上面的代码不会提供任何警告。

就我个人而言,在更新Noda Time时,我不会删除任何参数验证。


3
为什么不让 C# 8.0 编译器为非可空引用类型的方法参数生成 if (arg is null) throw IL 指令?如果编译器默认生成这段代码,那不是很好吗?请解释一下原因。 - Steven
5
@Steven:这将改变代码的行为,使其打开该功能变得更加危险。有一条明确的底线是“打开功能不会改变生成的IL代码”,这使得推理变得非常容易,并意味着您不会因为期望错误而以“可能被证明是无害”的方式破坏代码。我希望会有语法可以让您表达“此参数非空,请为我检查”,但我支持默认情况下不这样做的决定。 - Jon Skeet
2
谢谢Jon,我明白了。不过,我不确定你所说的附加语法。我更愿意看到编译器开关/项目设置来启用它(最好默认启用新创建的C# 8项目)。 - Steven
2
@Youssef13:运行时不会防止引用为空。你只会得到警告。所以变量“不可为空”或“变量不能为null”的情况并非如此。如果您的代码是唯一可以为某个路径提供值的代码,并且您绝对确定从未传递null(或者从何处派生该值),那么可以删除检查 - 但是如果该值可能来自其他地方(例如,调用者提供参数),则应进行检查,因为它可能为null。 - Jon Skeet
1
@JonSkeet 感谢您的回答和解释。我对可空引用类型有一个完全错误的想法。感谢您的纠正 :) - Youssef13
显示剩余3条评论

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