提问人:Nippysaurus 提问时间:12/22/2009 最后编辑:MattNippysaurus 更新时间:9/29/2022 访问量:35117
避免 null 引用异常
Avoiding null reference exceptions
问:
显然,代码中的绝大多数错误都是空引用异常。是否有任何通用技术可以避免遇到空引用错误?
除非我弄错了,否则我知道在 F# 等语言中不可能有 null 值。但这不是问题,我问的是如何在 C# 等语言中避免空引用错误。
答:
一种方法是尽可能使用 Null 值对象(又名 Null 对象模式)。这里还有更多细节
实际上,如果在您的语言中有 null 值,它一定会发生。空引用错误来自应用程序逻辑中的错误 - 因此,除非您可以避免所有这些错误,否则您一定会遇到一些错误。
适当使用结构化异常处理有助于避免此类错误。
此外,单元测试还可以帮助您确保代码按预期运行,包括确保值在不应为 null 时不为 null。
使用 Null 对象模式是这里的关键。
请确保在未填充集合的情况下要求集合为空,而不是 null。当空集合时使用 null 集合会令人困惑,而且通常是不必要的。
最后,我让我的对象在构造时尽可能断言非 null 值。这样一来,我以后就毫无疑问地知道值是否为 null,并且只需要在必要时执行 null 检查。对于我的大多数字段和参数,我可以根据以前的断言假设值不是 null。
我见过的最常见的空引用错误之一是来自字符串。将进行检查:
if(stringValue == "") {}
但是,字符串实际上是 null。它应该是:
if(string.IsNullOrEmpty(stringValue){}
此外,在尝试访问该对象的成员/方法之前,可能会过于谨慎并检查该对象是否为 null。
评论
在空引用导致异常之前,您可以轻松地检查它,但通常这不是真正的问题,因此您最终只会抛出异常,因为没有任何数据,代码就无法真正继续。
通常,主要问题不在于你有一个空引用,而在于你首先得到了一个空引用。如果引用不应为 null,则不应在没有正确引用的情况下通过初始化引用的点。
你没有。
或者更确切地说,在 C# 中尝试“防止”NRE 没有什么特别的可做的。在大多数情况下,NRE 只是某种类型的逻辑错误。您可以通过检查参数和大量代码(例如
void Foo(Something x) {
if (x==null)
throw new ArgumentNullException("x");
...
}
到处都是(大部分 .Net Framework 都是这样做的),所以当你搞砸了时,你会得到一个信息量稍大的诊断(不过,堆栈跟踪更有价值,NRE 也提供了这一点)。但你最终还是有一个例外。
(旁白:像这样的异常 - NullReferenceException、ArgumentNullException、ArgumentException ... - 通常不应该被程序捕获,而只是意味着“此代码的开发人员,有一个错误,请修复它”。我将这些称为“设计时”例外;将这些与真正的“运行时”异常进行对比,这些异常是由于运行时环境(例如 FileNotFound)而发生的,并且可能被程序捕获和处理。
但归根结底,你只需要正确地编码它。
理想情况下,大多数 NRE 永远不会发生,因为“null”对于许多类型/变量来说是一个无意义的值,理想情况下,静态类型系统将不允许将“null”作为这些特定类型/变量的值。然后,编译器将阻止您引入这种类型的意外错误(排除某些类型的错误是编译器和类型系统最擅长的)。这是某些语言和类型系统的优势所在。
但是,如果没有这些功能,您只需测试代码以确保没有此类错误的代码路径(或者可能使用一些可以为您进行额外分析的外部工具)。
评论
除了上述(空对象,空集合)之外,还有一些通用技术,即C++的资源获取初始化(RAII)和Eiffel的按合同设计。这些归结为:
- 使用有效值初始化变量。
- 如果变量可以为 null,则检查 null 并将其视为特殊情况,或者预期 null 引用异常(并处理该异常)。断言可用于测试开发版本中的合约违规。
我看过很多代码,看起来像这样:
if ((value != null) && (value.getProperty() != null) && ... ) doSomethingUseful();
很多时候,这是完全不必要的,大多数测试可以通过更严格的初始化和更严格的合约定义来删除。
如果这是代码库中的问题,那么有必要了解 null 在每种情况下所代表的含义:
- 如果 null 表示空集合,则使用空集合。
- 如果 null 表示异常情况,则引发 Exception。
- 如果 null 表示意外未初始化的值,请显式初始化它。
- 如果 null 表示合法值,请对其进行测试 - 或者最好使用执行 null 运算的 NullObject。
在实践中,这种设计级别的清晰度标准并非易事,需要努力和自律才能始终如一地应用于您的代码库。
避免 NullReferenceExceptions 的最简单方法之一是主动检查类构造函数/方法/属性设置器中的 null 引用,并引起对问题的注意。
例如
public MyClass
{
private ISomeDependency m_dependencyThatWillBeUsedMuchLater
// passing a null ref here will cause
// an exception with a meaningful stack trace
public MyClass(ISomeDependency dependency)
{
if(dependency == null) throw new ArgumentNullException("dependency");
m_dependencyThatWillBeUsedMuchLater = dependency;
}
// Used later by some other code, resulting in a NullRef
public ISomeDependency Dep { get; private set; }
}
在上面的代码中,如果传递 null ref,您将立即发现调用代码错误地使用了该类型。如果没有空引用检查,则可以通过许多不同的方式掩盖错误。
你会注意到,.NET Framework 库几乎总是在早期失败,而且如果你提供无效的 null 引用,则经常会失败。由于抛出的异常明确地指出“你搞砸了!”并告诉你原因,这使得检测和纠正有缺陷的代码成为一项微不足道的任务。
我听到一些开发人员抱怨说这种做法过于冗长和冗余,因为您只需要 NullReferenceException,但在实践中我发现它有很大的不同。如果调用堆栈很深和/或参数已存储,并且其使用被推迟到以后(可能在不同的线程上或以其他方式被掩盖),则尤其如此。
你更愿意拥有什么,入口方法的 ArgumentNullException,或者它的内脏中有一个模糊的错误?离错误源越远,跟踪它就越困难。
好的代码分析工具可以在这方面提供帮助。如果你使用的工具将 null 视为代码的可能路径,那么良好的单元测试也会有所帮助。尝试在生成设置中抛出“将警告视为错误”的开关,看看是否可以在项目中保留 # of warnings = 0。你可能会发现警告告诉你很多。
要记住的一件事是,抛出 null - reference 异常可能是一件好事。为什么?因为这可能意味着应该执行的代码没有执行。初始化为默认值是个好主意,但应注意不要最终隐藏问题。
List<Client> GetAllClients()
{
List<Client> returnList = new List<Client>;
/* insert code to go to data base and get some data reader named rdr */
for (rdr.Read()
{
/* code to build Client objects and add to list */
}
return returnList;
}
好吧,这可能看起来没问题,但根据您的业务规则,这可能是一个问题。当然,你永远不会抛出一个空引用,但也许你的 User 表不应该是空的?你是希望你的应用在原地旋转,从用户那里生成支持电话,说“这只是一个空白屏幕”,或者你是否想引发一个可能记录在某个地方的异常并快速发出警报?不要忘记验证您正在执行的操作以及“处理”异常。这就是为什么有些人不愿意从我们的语言中删除空值的原因之一......它使找到错误变得更加容易,即使它可能会导致一些新的错误。
请记住:处理异常,不要隐藏它们。
当向用户显示空引用异常时,这表示代码中存在缺陷,这是由于开发人员的错误造成的。以下是有关如何防止这些错误的一些想法。
对于那些关心软件质量,并且也在使用 the.net 编程平台的人来说,我的首要建议是安装和使用Microsoft代码契约(http://msdn.microsoft.com/en-us/devlabs/dd491992.aspx)。它包括执行运行时检查和静态验证的功能。将这些合约构建到代码中的基本功能包含在 4.0 版本的 the.net 框架中。如果你对代码质量感兴趣,并且听起来像你感兴趣,你可能真的很喜欢使用 Microsoft 代码协定。
使用 Microsoft 代码协定,可以通过添加类似“Contract.Requires(customer != null);”的前提条件来保护方法免受 null 值的影响。添加这样的前提条件等同于许多其他人在上面的评论中推荐的做法。在代码协定之前,我建议你做这样的事情
if (customer == null) {throw new ArgumentNullException("customer");}
现在我推荐
Contract.Requires(customer != null);
然后,您可以启用运行时检查系统,该系统将尽早捕获这些缺陷,从而引导您诊断和纠正有缺陷的代码。但不要让我给你的印象是,代码协定只是替换参数 null 异常的一种奇特方式。他们比这强大得多。 使用 Microsoft 代码协定,还可以运行静态检查器,并要求它调查代码中可能发生空引用异常的站点。静态检查器需要更多的经验才能轻松使用。我不会先向初学者推荐它。但请随意尝试一下,亲眼看看。
关于零参考误差发生率的研究
在这个线程中,关于空引用错误是否是一个重大问题,存在一些争论。下面是一个啰嗦的答案。对于那些不想涉足的人,我会总结一下。
- Microsoft 在以下领域的领先研究人员 Spec# 上的程序正确性和 代码合同项目认为它是 一个值得解决的问题。
- Bertrand Meyer 博士和 ISE 的软件工程师,谁 开发和支持埃菲尔铁塔 编程语言,也相信它 是一个值得解决的问题。
- 在我自己开发普通软件的商业经验中,我经常看到空引用错误,因此我想在我自己的产品和实践中解决这个问题。
多年来,Microsoft 一直在投资旨在提高软件质量的研究。他们的努力之一是 Spec# 项目。在我看来,the.net 4.0 框架最令人兴奋的发展之一是引入了Microsoft代码契约,这是 Spec# 研究团队早期工作的产物。
关于你说的“代码中的绝大多数错误都是空引用异常”,我相信是限定词“绝大多数”会引起一些分歧。短语“绝大多数”表明,可能 70-90% 的故障的根本原因都是空引用异常。这在我看来太高了。我更喜欢引用 Microsoft Spec# 的研究。在他们的文章 The Spec# programming system: An overview,作者是 Mike Barnett、K. Rustan M. Leino 和 Wolfram Schulte。在 CASSIS 2004, LNCS vol. 3362, Springer, 2004 中,他们写道
1.0 非 Null 类型 现代程序中的许多错误表现为 null 取消引用错误,提示 编程的重要性 语言提供以下能力 区分 计算结果可能为 null 和那些 肯定不会(对于一些实验 证据,见[24,22])。事实上,我们 想要根除所有 null 取消引用错误。
对于熟悉这项研究的Microsoft人员来说,这可能是一个来源。本文可在 Spec# 站点上找到。
我复制了下面的参考文献 22 和 24,并为方便起见附上了 ISBN。
曼努埃尔·法恩德里希(Manuel Fahndrich)和K·鲁斯坦·莱诺(K. Rustan M. Leino)。声明和检查非 null 类型 面向对象的语言。2003 年 ACM 面向对象会议论文集 Programming, Systems, Languages, and Applications, OOPSLA 2003, 卷 38, 编号 11 SIGPLAN 通知,第 302-312 页。ACM,2003年11月。国际标准书号 = {1-58113-712-5},
科马克·弗拉纳根、K·鲁斯坦·莱诺、马克·利利布里奇、格雷格·纳尔逊、詹姆斯·萨克斯、 和雷米·斯塔塔(Raymie Stata)。Java 的扩展静态检查。在 2002 年 ACM 的会议记录中 SIGPLAN 编程语言设计与实现会议 (PLDI),卷 37,SIGPLAN 通知第 5 号,第 234-245 页。ACM,2002年5月。
我查看了这些参考资料。第一个引用表示他们所做的一些实验,检查了自己的代码,以查找可能的空引用缺陷。他们不仅发现了几个,而且在许多情况下,识别出潜在的空参考表明设计存在更广泛的问题。
第二个引用没有提供任何具体证据来断言 null 引用错误是有问题的。但作者确实指出,根据他们的经验,这些空引用错误是软件缺陷的重要来源。然后,本文继续解释他们如何试图消除这些缺陷。
我还记得在ISE关于最近发布的Eiffel的公告中看到了一些关于这一点的内容。他们将这个问题称为“空隙安全”,就像伯特兰·迈耶博士启发或开发的许多事情一样,他们对这个问题以及他们如何用他们的语言和工具预防它进行了雄辩和教育性的描述。我建议您 http://doc.eiffel.com/book/method/void-safety-background-definition-and-tools 阅读他们的文章以了解更多信息。
如果您想了解有关 Microsoft 代码协定的更多信息,最近出现了大量文章。你也可以在 http 上查看我的博客:SLASH SLASH codecontracts.info 它主要致力于通过使用合同编程来讨论软件质量。
评论
如果存在可以替换 null 的合法对象,则可以使用 Null 对象模式和特殊情况模式。
在无法构造此类对象的情况下,由于根本无法实现其强制操作,您可以依赖空集合,例如在Map-Reduce查询中。
另一种解决方案是 Option 函数类型,即包含零个或一个元素的集合。这样,您将有机会跳过无法执行的操作。
这些选项可以帮助您编写代码,而无需任何空引用和任何空检查。
纯代码解决方案
您始终可以通过将变量、属性和参数标记为“不可为空”来创建一个结构,以帮助更早地捕获 null 引用错误。下面是一个在概念上按照工作方式建模的示例:Nullable<T>
[System.Diagnostics.DebuggerNonUserCode]
public struct NotNull<T> where T : class
{
private T _value;
public T Value
{
get
{
if (_value == null)
{
throw new Exception("null value not allowed");
}
return _value;
}
set
{
if (value == null)
{
throw new Exception("null value not allowed.");
}
_value = value;
}
}
public static implicit operator T(NotNull<T> notNullValue)
{
return notNullValue.Value;
}
public static implicit operator NotNull<T>(T value)
{
return new NotNull<T> { Value = value };
}
}
您的使用方式与使用的方式非常相似,只是其目标是实现完全相反的 - 不允许 。以下是一些示例:Nullable<T>
null
NotNull<Person> person = null; // throws exception
NotNull<Person> person = new Person(); // OK
NotNull<Person> person = GetPerson(); // throws exception if GetPerson() returns null
NotNull<T>
隐式转换,因此您几乎可以在任何需要的地方使用它。例如,您可以将一个对象传递给一个方法,该方法采用:T
Person
NotNull<Person>
Person person = new Person { Name = "John" };
WriteName(person);
public static void WriteName(NotNull<Person> person)
{
Console.WriteLine(person.Value.Name);
}
如上所述,与 nullable 一样,您将通过属性访问基础值。或者,您可以使用显式或隐式强制转换,您可以看到返回值如下的示例:Value
Person person = GetPerson();
public static NotNull<Person> GetPerson()
{
return new Person { Name = "John" };
}
或者,您甚至可以在方法返回时使用它(在本例中),方法是进行强制转换。例如,以下代码就像上面的代码一样:T
Person
Person person = (NotNull<Person>)GetPerson();
public static Person GetPerson()
{
return new Person { Name = "John" };
}
与扩展相结合
结合扩展方法,您可以涵盖更多情况。下面是扩展方法的示例:NotNull<T>
[System.Diagnostics.DebuggerNonUserCode]
public static class NotNullExtension
{
public static T NotNull<T>(this T @this) where T : class
{
if (@this == null)
{
throw new Exception("null value not allowed");
}
return @this;
}
}
下面是一个如何使用它的示例:
var person = GetPerson().NotNull();
GitHub的
为了供您参考,我在 GitHub 上提供了上面的代码,您可以在以下位置找到它:
https://github.com/luisperezphd/NotNull
可以提供帮助的工具
还有几个库可以提供帮助。上面提到了 Microsoft 代码协定。
其他一些工具包括 Resharper,它可以在你编写代码时为你提供警告,特别是当你使用它们的属性时:NotNullAttribute
还有 PostSharp,它允许您使用如下属性:
public void DoSometing([NotNull] obj)
通过这样做并使 PostSharp 成为构建过程的一部分,将在运行时检查是否为 null。请参见: PostSharp 空检查obj
Fody 代码编织项目有一个用于实现空守卫的插件。
当在程序集中找不到方法时,可以显示 NullReferenceException,对于 例如 m0=mi.GetType()。GetMethod(“TellChildToBeQuiet”),其中程序集是 SportsMiniCar,mi 是 MiniVan 的实例,TellChildToBeQuiet 是程序集中的一个方法。 我们可以通过查看包含上述方法的程序集版本 2.0.0.0 放置在 GAC 中来避免这种情况。 示例:使用参数调用方法:'
enter code here
using System;
using System.Rwflection;
using System.IO;
using Carlibraries;
namespace LateBinding
{
public class program
{
static void Main(syring[] args)
{
Assembly a=null;
try
{
a=Assembly.Load("Carlibraries");
}
catch(FileNotFoundException e)
{
Console.Writeline(e.Message);
Console.ReadLine();
return;
}
Type miniVan=a.GetType("Carlibraries.MiniVan");
MiniVan mi=new MiniVan();
mi.TellChildToBeQuiet("sonu",4);
Console.ReadLine();
}
}
}
记得使用 TellChildToBeQuiet(string ChildName,int count) 更新 MiniSportsCar Assembly
在没有适当的“else case”的情况下成功避免 null 意味着现在您的程序不会失败,但它也不会纠正。除非整个 java api 返回 optional,否则 Optional 也帮不了你,但到那时,你被迫到处检查什么都没有,就好像到处检查 null 一样。毕竟这没什么区别。
将来人们可能会发明另一个对象“可伪造”,以避免不检查就返回错误!哈哈
只有理解逻辑并根据需要进行检查才能为您提供帮助。非可选。这只是虚假的安全。
除了本次讨论中给出的其他有用提示外,这里还有一个额外的提示:
从 C# 8 开始,您可以使用
#nullable enable
这将启用编译时 null 检查,并将识别可能发生 null 引用异常的所有区域,并强制您通过添加 null 检查或引发已知异常来修复它。可以在此处找到 C# 代码示例,Microsoft 在此处进行了非常详细的记录。
示例(将其粘贴到 LinqPad 中):
// #nullable enable
int? a;
int b;
int c;
void Main()
{
c = (int)a + b;
(a, b, c).Dump();
}
此代码在运行时抛出,因为没有赋值且为 null,因此添加无效。InvalidOperationException
a
现在,如果删除 和 启用,您甚至可以在运行代码之前看到,编译器会警告您代码可能会导致错误: IntelliSense 下划线 。//
#nullable enable
CS8629 Nullable value type might be null
(int) a
从 C#8 和 .NET6(C#10) 默认情况下,可为 null 的引用类型会改变游戏规则。
一些参考资料:
评论
null
x = null