如何在 c 中识别可能导致 NullReferenceException 的所有可能的代码块#

How to identify all possible code blocks which can cause the NullReferenceException in c#

提问人:Nilesh Wagh 提问时间:4/8/2021 最后编辑:marc_sNilesh Wagh 更新时间:4/9/2021 访问量:175

问:

我在 .NET 4.7.2 中使用 C# 和 Entity Framework 的一个项目在多种情况下都会导致这种情况。代码库非常庞大,因此在使用之前分析代码和处理可为 null 的对象非常困难。NullReferenceException

当我们尝试使用实体框架访问给定实体的相关 null 实体或 null 实体集合(通过外键相关)时,会发生大多数异常。如果外键表中不存在数据,则该相关实体将存在或为 null。

我们假设相关对象过去一定存在,但现在表的大部分结构已经更改,这些对象现在可能不存在。

我的代码太长,很难分析和更改可为 null 的对象处理。

那么有人可以向我建议解决方案,该解决方案将给我一个将来会导致的代码块?NullReferenceException

有什么工具可以给我一个警告,在哪里会发生?NullReferenceException

C# 实体框架 NullReferenceException 代码分析

评论

1赞 canton7 4/8/2021
查看 C# 8 中引入的“可以为 null 的引用类型”功能,该功能旨在实现此目的
1赞 Jeroen Mostert 4/8/2021
这或多或少是可为 null 的引用类型旨在帮助解决的那种方案。如果有必要,您可以逐个方法或逐个类逐渐选择加入这些选项,并发现代码假设某些东西不会在可能的情况下出现的情况。null
0赞 Gert Arnold 4/8/2021
该工具是具有足够代码覆盖率的单元测试。
0赞 Nishan 4/8/2021
尝试使用声纳棉绒 sonarlint.org/visualstudio 来识别可能的代码异味。
0赞 Sergey Vasiliev 4/9/2021
您也可以尝试使用 PVS-Studio 静态代码分析器。分析器的某些诊断规则可以检测潜在空引用的取消引用。

答:

3赞 Steve Py 4/9/2021 #1

NullReferenceException是你从第一天起就必须考虑的事情。在始终设置引用的假设下编写代码,这为错误敞开了大门,这些错误通常是将来出现的间歇性错误。AFAIK 没有针对该问题的“快速解决方案”,但您可以采取重构步骤来减少 Null 的影响,并帮助确保在调用堆栈实际有用的位置发生意外 null 值的异常:

#1.从 Linq 表达式中删除所有方法,这些方法未显式处理数据可能不会返回的事实。这意味着替换为或更好,您期望 1 条记录。您仍然会遇到异常,但会在读取实体时明确哪些条件无效,而不是稍后在代码中使用实体而不检查 #null。*OrDefault()FirstOrDefault()First()Single()

#2.初始化实体中的集合属性。这有助于确保实体中的集合在创建新实体时“准备就绪”,以防接收实体的函数可能会获得新行而不是现有行。J.F.

public virtual ICollection<OrderLine> OrderLines { get; set; } = new List<OrderLine>();

#3.确保启用延迟加载,并将引用和集合标记为 。这绝不是理想的,但恕我直言,延迟加载调用比 .如果调用恰好在加载实体的 DbContext 范围之外进行,则仍会收到异常,但通常更容易确定访问的内容。virtualNullReferenceException

#4.添加快速失败验证。在接受存储的构造函数参数时,请添加 null 检查断言。对于接受任何可能为 null 的内容的方法,请添加断言。你会得到一些例外,但有意义的例外会帮助你识别错误的来源。例如:

// Constructor injected references:
oublic OrderService(IOrderRepository repository)
{
    // change this:
    // _repository = repository;

    // ... to this:
    _repository = repository ?? throw new ArgumentNullException("repository");
}

// Parameter assertion:

public void AddCustomer(Customer customer)
{   
    // add these to assert your parameters.
    if(customer == null) throw new ArgumentNullException("customer");

    // ...
}

这些通常是快速的重构,可以在整个现有应用程序中非常不显眼地应用。

#5.旨在消除除插入新实体之外构造实体的任何代码。我遇到的常见例子是:

.Select(x => new Order { /* populating some Order columns */ })

其中 是 Entity 类,但像视图模型或其他一些临时数据容器一样使用。任何其他代码,如果不是出于使用目的,那么它就应该被重构,并带有偏见。实体类应始终反映数据的完整或可完成的表示形式。(可完成意味着被跟踪的实体仍在其 DbContext 范围内,并且能够在需要时延迟加载)任何被调用接受 Order 实体的函数都应该收到一个完整且有效的 Order,其中包含所有引用,无论是 eager load 还是 lazy loadable。传递的所有实体也应全部关联到同一个 DbContext,因此理想情况下,应将实体序列化到 Web 客户端和从 Web 客户端序列化实体的代码,以使用投影,或者非常谨慎地确保实体重新关联到当前作用域的 DbContext。Ordernewcontext.Orders.Add(order);

归根结底,你面临的是一种形式的技术债务,就像所有债务一样,你早期在通过假设节省开发时间方面获得的“信用”会产生利息,这将增加以后追踪错误并最终“修复”错误假设的时间。您需要从童子军的角度来看待应用程序,并致力于让它比开始时更干净。

评论

0赞 Gert Arnold 4/9/2021
你在这里提出了一些好的观点。但请注意,EF6 LINQ 查询并不总是受支持,并且有许多有效的情况。另外,我不建议延迟加载,IMO 它接近反模式。我更喜欢成员集合的延迟初始化,就像这个答案一样。First()FirstOrDefault()