如何避免空指针出错?

栏目: Java · 发布时间: 6年前

内容简介:一家专门帮助开发人员了解生产中发生问题的以色列公司OverOps,对生产过程中出现的最重要的java异常进行了在C#和Java中,所有引用类型都可以指向null。我们可以通过以下方式获得指向null的引用:

一家专门帮助开发人员了解生产中发生问题的以色列公司OverOps,对生产过程中出现的最重要的 java 异常进行了 研究 。猜猜哪一个处于第一个?空指针异常。并不是因为开发人员忘记添加空值检查,而是因为开发人员过多使用空值。

所以这些NULL来自何处?

在C#和Java中,所有引用类型都可以指向null。我们可以通过以下方式获得指向null的引用:

  • “未初始化”的引用类型变量 - 使用空值初始化并随后赋予其实际值的变量。错误可能导致它们永远不会被重新分配。
  • 未初始化的引用类型类成员。
  • 显式赋值为null或从函数返回null

以下是我注意到函数返回null的一些模式:

1. 错误处理

输入无效时返回null。这是返回错误代码的类似方法。我认为这是一种旧式的编程风格,起源于不存在例外的时候。

2.实体的可选数据

实体的属性可以是可选的。如果没有可选属性的数据,则返回null。

<b>public</b> <b>class</b> Person
{
    <b>public</b> string FirstName { get; set; }
    <font><i>// can return null</i></font><font>
    <b>public</b> string MiddleName { get; set; }
    <b>public</b> string LastName { get; set; }
}
</font>

2.分层模型

在分层模型中,我们通常可以上下导航。当我们处于顶端时,我们需要一种方式来表达,通常是返回null。

<b>interface</b> IEmployee
{
    string FullName { get; }
    string IdNumber { get; }
    ...
    <font><i>// returns null when the IEmployee is the CEO, because CEO don't have a manager</i></font><font>
    IEmployee Manager { get; }
    IEnumerable<IEmployee> ManagerOf{ get; }
}
<b>interface</b> ITreeNode<T>
{
    </font><font><i>// returns null when the node is the root</i></font><font>
    ITreeNode<T> Parent { get; }
    IEnumerable<ITreeNode<T>> Children{ get; }
    T Data { get; }    
}
</font>

3.查找功能

当我们想要通过集合中的某些条件查找实体时,我们返回null作为表示未找到实体的方式。

当我们想要根据特定条件在集合中找到一个实体时,我们返回null作为一种说法找不到该实体的方式

<b>class</b> Employe 
{
  string Id { get; set;}
  string Name {get; set;}
  ...
}

<b>class</b> Company
{     
  <font><i>// returns null if there is no employe with that id</i></font><font>
  <b>public</b> Employe FindEmployeById(string id)
  {
    ...
  }    
}
</font>

使用空值有什么问题?

引发NullPointerException的代码可能离fix bug很远。它使得追踪真正的问题更加困难。特别是如果代码是分支的。

在下面的代码示例中,有一个错误,在A类的某个地方,导致实体为null。但是NullPointerException是在类B的函数内引发的。可以想象现实代码可能要复杂得多。

<b>class</b> A
{
  <b>void</b> DoSomething1()
  {
      Entity1 entity = <b>null</b>;
      <b>if</b> (...)
      {
        <b>if</b> (...)
        {
          entity = CreateEntity(1);
        }
      }
      <b>else</b>
      {
        <b>if</b> (...)
        {
          entity = CreateEntity(2);
        }
      }
      
      DoSomething2(entity);
  }
  <b>void</b> DoSomething2(Entity1 entity)
  {
    <b>if</b>(...)
    {
      <b>new</b> B().DoSomething(entity);
    }
  }
}

<b>class</b> B
{
  <b>void</b> DoSomething(Entity1 entity)
  {
    <b>if</b> (...)
    {
      <b>var</b> x = entity.Prop1;
    }
  }
}

第二:会隐藏了错误:我遇到了空检查,看起来开发人员正在思考“我知道我应该检查null但我不知道当函数返回null时它意味着什么,我不知道该怎么做”或“我认为这不能为空,但只是为了确保,我不希望它破坏生产“。它通常看起来像这样:

 <b>public</b> <b>void</b> registerItem(Item item)
 {
     <b>if</b> (item != <b>null</b>) {
         ItemRegistry registry = peristentStore.getItemRegistry();
         <b>if</b> (registry != <b>null</b>) {
             Item existing = registry.getItem(item.getID());
             <b>if</b> (existing.getBillingPeriod().hasRetailOwner())
             {
                 existing.register(item);
             }
         }
     }
 }

这种类型的空检查会导致某些逻辑在没有了解它的情况下不会发生。编写这种代码意味着流的某些逻辑失败但整个流程成功。它还可能导致某些其他功能中的错误,这些功能假定其他功能完成了它的工作。

想象一下,你在网上购买一张演出门票。你有成功的消息!节目的最后一天到了,你早点下班,安排一个保姆,然后去看节目。当你到达时发现你没有门票!而且没有空座位。你回家困难和困惑。你能看到这种空值检查会如何导致这种情况吗?

缺少C#和Java中的非Nullable引用类型

在C#和Java中,引用类型始终指向null。如果null是它的有效输入或输出,通过查看函数方法签名我们是无法了解情况的,我相信大多数函数不会返回null或接受null作为入参。

因为很难知道函数是否返回null(除非有记录),开发人员要么在不需要时进行空检查,要么在需要时不检查空值(是的,有时在需要时进行空检查。

这种糟糕的设计选择导致我之前在“隐藏错误”中描述的问题和当然很多NullPointerException。失败的情况。

Kotlin 这样的语言旨在通过区分可空引用和不可空引用来消除NullPointerException异常。这允许捕获分配给非空引用的null,并确保开发人员在解除引用可引用引用之前检查null,所有这些都在编译时就会实现。

Microsoft通过在C#8中引入 Nullable引用类型 采用相同的方法。

那我们该怎么办?听取鲍勃叔叔的意见

众所周知的“鲍勃叔叔” 罗伯特·C·马丁 写了一本关于清洁代码的最着名的书籍(令人惊讶的是) “清洁代码” 。在Uncle Bob声称的这本书中,我们不应该返回null,也不应该将null传递给函数。

消除试验Null的技术模式

我并不是说这是每个场景的最佳解决方案。

1. 将功能拆分为两个

返回null的每个函数都将转换为2个函数。一个具有相同签名的函数抛出异常而不是返回null。第二个函数返回一个布尔值,表示它是否有效以调用第一个函数。我们来看一个例子:

<b>class</b> Employe 
{
  string Id { get; set;}
  string Name {get; set;}
  ...
}

<b>class</b> Company
{ 
  <b>public</b> bool ContainsEmployeById(string id)
  {
    ...
  }
 

//如果没有该id的雇员,将会引发异常

  <b>public</b> Employe FindEmployeById(string id)
  {
    ...
  }    
}
<b>void</b> PayEmploye(HttpRequest httpRequest,Company company)
{
  string id = GetID(httpRequest)  
  <b>if</b> (company.ContainsEmployeById(id))
  {
    <b>var</b> employe = company.FindEmployeById(id);
    PayEmploye(employe);
  }
  <b>else</b>
  {
    ResponseWithError(String.Format(<font>"employe with id {0} don't exist"</font><font> , id));
  }
}
</font>

ContainsEmployeById的逻辑基本上与FindEmployeById相同,但不返回员工。这里会遇到DB性能问题。让我们介绍一个类似但不同的模式:返回true时的布尔函数也会返回我们搜索的数据。它看起来像这样:

<b>class</b> Company
{     
 

//如果没有该id的雇员,将抛出异常

  <b>public</b> Employe FindEmployeById(string id)
  {
    ...
  }    
  
 

//如果没有该id的雇员,则返回false

  <b>public</b> bool TryFindEmployeById(string id , ref Employe employe)
  {
  }
}
<b>void</b> PayEmploye(HttpRequest httpRequest,Company company)
{
  string id = GetID(httpRequest);
  Employe employe;
  <b>if</b> (company.FindEmployeById(id , ref employe))
  {  
    PayEmploye(employe);
  }
  <b>else</b>
  {
    ResponseWithError(String.Format(<font>"employe with id {0} don't exist"</font><font> , id));
  }
}
</font>

这种模式的一个常见用途是 int.Parseint.TryParse

事实上,我可以将一个函数分成两个函数,每个函数都有自己的用法,这表明返回null是有违反单一责任原则的代码气味。

拆分界面

我们可以从 Liskov原则 推导出的实用指南是类必须实现它实现的接口的所有功能。返回null或抛出异常是不实现函数的方法。因此,返回null是违反Liskov原则的代码气味。

如果一个类不能实现特定接口的函数,我们可以将该函数移动到另一个接口,每个类只实现它可以的接口。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Two Scoops of Django

Two Scoops of Django

Daniel Greenfeld、Audrey M. Roy / CreateSpace Independent Publishing Platform / 2013-4-16 / USD 29.95

Two Scoops of Django: Best Practices For Django 1.5 is chock-full of material that will help you with your Django projects. We'll introduce you to various tips, tricks, patterns, code snippets, and......一起来看看 《Two Scoops of Django》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具