如何强制进行空值检查?

15

我正在一个大型项目上工作,即使有成千上万个自动化测试和100%的代码覆盖率,我们仍然会遇到大量错误。我们大约95%的错误都是NullReferenceExceptions。

有没有办法在编译时强制执行空值检查?

如果不能实现,是否有任何自动化方法可以在单元测试中强制执行空值检查而无需手动编写这些测试?


1
空引用异常是来自测试框架还是被测试的实际代码本身? - Matthew Iselin
哪个构建服务器?如果是TFS,可以使用代码分析策略规则来帮助。 - Yoann. B
1
也许可以在您的代码风格检查器中增加一条规则,用于查找{} = nullreturn null;?如果您从未将任何东西设置为null,则唯一需要检查null的是库调用的结果。 - Anon.
3
当然还有未初始化的类成员变量。 - Patrick
@Matthew Iselin:异常来自代码,而不是测试框架。我们有一些自动化的端到端系统和集成测试,似乎工作得足够好,但许多空指针异常是由我们的QA测试人员或用户在现场发现的。 - Juliet
显示剩余2条评论
13个回答

18
你应该了解一下代码契约(Code Contracts)。 静态检查器只适用于高级Visual Studio版本,但基本上这就是你需要的。
有很多在线资源,<plug>你还可以免费阅读C# in Depth第二版的代码契约章节预发布版本 - 下载第15章</plug>。(与最新版本的代码契约相比,该章节略有过时,但没有太大影响。)

+1 代码契约绝对可以在编译时彻底解决空引用问题。只有在消除了将可能传递到特定方法/类的空值可能性后,才能构建代码。同时还应该查看与代码契约配合使用的 Pex。 - sidney.andrews
@Jon Skeet:我错了还是代码合同只有在开发人员在代码中使用Requires.Something时才起作用?所以如果开发人员在使用合同方面犯了错误,它会在编译时通过吗?我认为Juliet想在开发后的测试或构建时检查这个问题。 - Yoann. B
@Yoann:是的,你必须在代码中表达合同。否则,你怎么区分哪些API可以接受null,哪些不行呢?但是静态检查器确实会在编译时检查API的调用者。 - Jon Skeet
@Jon:这就是为什么我建议使用自定义代码分析规则,但不确定是否可以构建检查空引用的自定义规则。 - Yoann. B
感谢您提供明确、建设性的答案。链接的章节非常棒。它会一直保持可用吗?我想链接到它... - Pascal Cuoq
显示剩余4条评论

6
C# 8引入了非空引用类型
可以修改 .Net 项目以启用 Nullable 选项:
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>

编译器将能够区分:
  • stringstring?

  • NonNullableClassNullableClass?


4

百分之百的代码覆盖率毫无意义。

这是一种虚假的安全感。

你所衡量的唯一指标就是你执行了所有的代码行。

而不是:

  • 这些代码行是否应该存在
  • 这些代码行是否正常运行(你有测试所有的边缘情况吗?)

例如,如果你处理火灾的程序只有一个步骤“逃离大楼”,即使在100%的情况下都这样做,也许一个更好的程序是“通知消防部门,尝试扑灭火势,然后如果万一全部失败再逃离”。

如果没有特别添加代码,C# 中没有任何内置的功能可以帮助你解决这个问题,你需要使用代码合同(.NET 4.0)或特定的 IF 语句(<4.0)。


1
更正:代码覆盖率确实有意义,但它并不代表一切。 - Luis Perez

1
有没有办法在编译时强制进行空值检查?
不行。编译器无法确定运行时引用变量是否指向 null。
而且排除 null 产生的语句(设置和返回)也是不够的。考虑以下情况:
public class Customer
{
  public List<Order> Orders {get;set;}
}
  //now to use it
Customer c = new Customer;
Order o = c.Orders.First(); //oops, null ref exception;

1
这不是一个技术解决方案,而是一个社交解决方案。当引用类型被外部代码(另一个方法调用等)修改时,在您的环境中访问引用类型而没有检查 null 应该是不可接受的。单元测试不能取代良好的传统代码审查。

这会导致成千上万行的新代码,但实际上并没有增加多少价值。如果你得到了一个空值,而且无法处理它,不要检查它,直接崩溃。更好的约定是“在生产代码中永远不要传递空引用给其他人(在适用的测试代码中使用null可以减少混乱)”。 - sara
@kai - 这太疯狂了。你不能让生产应用程序崩溃并失效,而且你无法控制第三方API返回null的方法。 - Payton Byrd
1
最好崩溃(或至少终止当前操作/请求),而不是吞下错误或让系统处于未知状态。当然,你不应该“让”你的应用程序崩溃,这种情况不应该发生。如果你在某个地方得到了一个null,那么你就有一个bug,你应该修复它,以便在那里不再得到null。当然,在应用程序边界、UI代码和第三方集成点上,你必须验证一些东西,但当你进入领域模型和业务逻辑时,null大多只会造成伤害并影响可读性。 - sara
我同意你在控制代码时不应该引入null,但你也不能忽略null并让错误冒泡。错误冒泡的层级越高,错误对于捕获错误的位置就越没有意义。因此,两个选择是将所有内容包装在try...catch中,或者测试null并进行优雅的处理。 - Payton Byrd
相反,你希望在从测试套件运行代码时能够插入nulls。这有助于显示真正重要的内容和不重要的内容。就像我说的,当然你必须验证用户输入或从Web请求中获取的内容等,但我认为,如果在域模型中得到了一个null并且没有预期到它,那么你就有一个bug,而且假装应用程序可以正常工作是愚蠢的。将所有内容包装在try/catch中或对每个LoC进行防御性检查,这正是你不想做的事情。不过,这已经变成了一次聊天,所以我退出了。 - sara

1

1) 我认为Resharper可以建议您检查代码中的一些关键位置。例如,它建议添加[空引用检查代码]并在您允许的情况下添加。

试试吧。当然,如果需要的话,它会增加您的经验。

2) 在应用程序开发的早期阶段,在您的代码中使用“快速失败”模式(或断言)。


1

防御式编程只能带你走到这里,也许最好的方法是捕获异常并像处理其他异常一样处理它。


在“处理”异常时,请确保处理发生异常的原因。为什么这个引用从来没有被设置过?在它被设置之前是否抛出了异常?今天我也遇到了这种情况,必须追踪其原因(缺少资源导致ArgumentNullException,已记录并忽略)。 - John Saunders
有些事情,特别是io操作,你永远无法确定是否有效。如果在某个地方拔掉电缆导致方法返回null(可能是不好的实践,但你并不总能得到想要的结果),那么最好将其作为异常捕获。 - CurtainDog

0

.NET框架希望通过使用!修饰符来强制执行编译时空引用检查。

public void MyMethod(!string cannotBeNull)

但不幸的是,我们没有编译时检查。您最好的选择是尽量减少外部调用者传递空值的次数,然后在公共方法中执行空检查:

public class ExternalFacing
{
  public void MyMethod(string arg)
  {
     if (String.IsNullOrEmpty(arg))
        throw new ArgumentNullException(arg);

     implementationDependency.DoSomething(arg);
   }
}

internal class InternalClass
{
    public void DoSomething(string arg)
    {
         // shouldn't have to enforce null here.
    }
}

然后对外部类应用适当的单元测试,以期望ArgumentNullExceptions异常。


1
不确定为什么这个被踩了,Spec#是一个真实存在的东西,起源于微软研究实验室。 http://research.microsoft.com/en-us/projects/specsharp/ Code Contracts是一个更好的选择,但我也没错。 - bryanbcook

0

0

在编译时,您无法进行空值检查,因为在编译时对象仅是类型,并且仅在运行时将类型转换为具有实际值的实例...这里是 null。


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