转载

如何避免空指针出错?

一家专门帮助开发人员了解生产中发生问题的以色列公司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.Parse 和 int.TryParse 。

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

拆分界面

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

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

原文  https://www.jdon.com/50517
正文到此结束
Loading...