显然,代码中的绝大多数错误都是空引用异常。是否有任何一般技术可以避免遇到空引用错误?
除非我弄错了,否则我知道在F#这样的语言中,不可能有空值。但那不是问题,我问如何避免使用C#等语言中的空引用错误。
答案 0 :(得分:30)
答案 1 :(得分:21)
除了上述(空对象,空集合)之外,还有一些通用技术,即资源获取是来自C ++的初始化(RAII)和来自Eiffel的Design By Contract。归结为:
我看过很多看起来像这样的代码:
if((value!= null)&&(value.getProperty()!= null)&& ...&&(... doSomethingUseful())
很多时候这是完全没必要的,大多数测试都可以通过更严格的初始化和更严格的合同定义来删除。
如果这是您的代码库中的问题,那么有必要在每种情况下理解null表示的内容:
在实践中,设计层面的这种清晰度标准并非易事,需要付出努力和自律才能始终如一地应用于您的代码库。
答案 2 :(得分:7)
你没有。
或者更确切地说,尝试在C#中“阻止”NRE并没有什么特别之处。在大多数情况下,NRE只是某种类型的逻辑错误。您可以通过检查参数和拥有大量代码(如
)来在接口边界处对这些进行防火墙处理void Foo(Something x) {
if (x==null)
throw new ArgumentNullException("x");
...
}
到处都是(大部分.Net Framework都这样做),所以当你搞砸了,你会得到一个稍微提供更多信息的诊断(虽然堆栈跟踪更有价值,NRE也提供了这个功能。 )。但你仍然只是一个例外。
(旁白:像这样的例外 - NullReferenceException,ArgumentNullException,ArgumentException,... - 通常不应该被程序捕获,而只是意味着“这个代码的开发者,有一个错误,请修复它”。我将这些视为“设计时”异常;将这些与运行时环境(例如FileNotFound)导致的真正“运行时”异常进行对比,并且可能被程序捕获和处理。)
但是在一天结束时,你只需要正确编码。
理想情况下,大多数NRE永远不会发生,因为'null'对于许多类型/变量来说是一个荒谬的值,理想情况下,静态类型系统会禁止'null'作为这些特定类型/变量的值。然后编译器会阻止您引入这种类型的意外错误(排除某些类型的错误是编译器和类型系统最擅长的)。这是某些语言和类型系统擅长的地方。
但是如果没有这些功能,您只需测试代码以确保没有此类错误的代码路径(或者可能使用一些可以为您进行额外分析的外部工具)。
答案 3 :(得分:5)
使用Null Object Patterns是关键。
确保在未填充集合时要求集合为空,而不是为空。当空集合执行时使用空集合会让人感到困惑,而且往往是不必要的。
最后,我尽可能在构造时使我的对象断言非空值。这样我后来无疑会知道值是否为null,并且只需执行空值检查必需。对于我的大多数字段和参数,我可以假设基于先前的断言,值不为空。
答案 4 :(得分:5)
您可以在导致异常之前轻松检查空引用,但通常这不是真正的问题,因此您最终会抛出异常,因为代码在没有任何数据的情况下无法真正继续。
通常,主要问题不在于您有一个空引用,而是您首先获得了一个空引用。如果引用不应该为null,那么在没有正确引用的情况下,不应超过引用初始化的点。
答案 5 :(得分:4)
如果您的语言中存在空值,则必然会发生。空引用错误来自应用程序逻辑中的错误 - 所以除非你能避免所有这些错误,否则你必然会遇到一些错误。
答案 6 :(得分:4)
我见过的最常见的空引用错误之一来自字符串。将有一张支票:
if(stringValue == "") {}
但是,字符串实际上是空的。它应该是:
if(string.IsNullOrEmpty(stringValue){}
此外,在尝试访问该对象的成员/方法之前,您可能过于谨慎并检查对象是否为空。
答案 7 :(得分:3)
一种方法是尽可能使用Null Value Objects(aka the Null Object Pattern)。有more details here
答案 8 :(得分:2)
适当使用结构化异常处理有助于避免此类错误。
此外,单元测试可以帮助您确保代码按预期运行,包括确保值不应该是空值。
答案 9 :(得分:2)
避免NullReferenceExceptions的最简单方法之一是在类构造函数/方法/属性设置器中积极检查空引用,并引起对问题的注意。
E.g。
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; }
}
在上面的代码中,如果你传递一个空引用,你会立即发现调用代码使用的类型不正确。如果没有空引用检查,则可以通过多种不同方式隐藏错误。
您会注意到,.NET框架库几乎总是提前失败,并且通常如果您提供空引用而无法执行此操作。由于抛出的异常明确表示“你搞砸了!”并告诉你原因,它使检测和纠正有缺陷的代码成为一项微不足道的任务。
我听到一些开发人员的抱怨,他们说这种做法过于冗长和多余,因为NullReferenceException就是你所需要的,但在实践中我发现它有很大的不同。如果调用堆栈很深和/或存储了参数并且其使用推迟到以后(可能在不同的线程上或以某种其他方式模糊),则尤其如此。
你更喜欢什么,在入口方法上有一个ArgumentNullException,或者在它的内容中有一个模糊的错误?你越远离错误的来源,追踪它就越难。
答案 10 :(得分:2)
良好的代码分析工具可以在这里提供帮助。如果您使用的工具将null视为代码中的可能路径,那么良好的单元测试也会有所帮助。尝试在构建设置中抛出该开关,将“将警告视为错误”并查看是否可以保留项目中的警告数量= 0.您可能会发现警告告诉您很多。
要记住的一件事是,它可能是一个好的东西,你抛出一个null引用异常。为什么?因为它可能意味着应执行的代码没有执行。初始化为默认值是一个好主意,但您应该小心,不要最终隐藏问题。
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表永远不应该是空的?您是否希望您的应用程序在适当的位置旋转,从用户那里产生“只是一个空白屏幕”的支持电话,或者您是否想要提出可能在某处记录并快速发出提醒的异常?不要忘记验证您正在做什么以及“处理”异常。这就是为什么有些人不愿意从我们的语言中取出空值的原因之一...它使得查找错误变得更容易,即使它可能会导致一些新错误。
记住:处理异常,不要隐藏它们。
答案 11 :(得分:1)
普通代码解决方案
您总是可以创建一个结构,通过将变量,属性和参数标记为&#34;而不是可空的&#34;来帮助先捕获空引用错误。以下是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);
}
正如您在上面看到的那样,您可以通过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上提供了上面的代码,您可以在以下网址找到它:
答案 12 :(得分:0)
如果可以存在可以替换null的合法对象,则可以使用Null Object pattern和Special Case pattern。
如果无法构造此类对象,因为根本无法实现其强制操作,您可以依赖空集合,例如Map-Reduce Queries。
另一种解决方案是Option functional type,它是具有零个或一个元素的集合。这样,您将有机会跳过无法执行的操作。
这些选项可以帮助您编写代码而不需要任何空引用和任何空检查。
答案 13 :(得分:0)
可提供帮助的工具
还有几个图书馆可以提供帮助。上面提到了Microsoft Code Contracts。
其他一些工具包括 Resharper ,它可以在您编写代码时为您提供警告,尤其是在您使用其属性时:NotNullAttribute
还有 PostSharp ,这样您就可以使用以下属性:
public void DoSometing([NotNull] obj)
通过这样做,并使构建过程obj
的PostSharp部分在运行时检查为null。请参阅:PostSharp null check
Fody代码编织项目有一个implementing null guards.
插件答案 14 :(得分:0)
在没有适当的“else case”的情况下成功避免 null 意味着现在您的程序不会失败,但也不会正确。 Optional 也帮不了你,除非整个 java api 都返回 optional ,但到那时,你不得不到处检查什么,就像到处检查 null 一样。毕竟这没什么区别。
未来人们可能会发明另一个对象“Falsable”,以避免不检查就返回false!哈哈
只有理解逻辑并根据需要检查才能帮助您。不可选。这只是虚假的安全。
答案 15 :(得分:-1)
当在程序集中找不到方法时,可以显示NullReferenceException ex 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更新MiniSportsCar程序集(字符串ChildName,int count)