背景
最近在做公司系统的模块化重构,发现了大量的函数和方法会在一定情况下返回一个Null值。虽然我能理解当时作者一定是想告诉我们此函数或方法在一定条件下返回空值。但是真的使用返回Null的方式来表达返回空值真的合适吗?
知识回顾
先从语意上理解什么是Null?
我从WIKIpedia上选了靠前的、编程角度上来说关于Null的解释。
Null 是一特殊指针值(或是一种 对象引用)表示这个指针并不指向任何的对象。这样的指针称之为 Null指针[1]
--WIKIpedia
计算机专业出身的同学应该能够很好理解这段话的意思。意思是说Null的类型是指针(因为它是Null指针)。
在许多定义里,Null 意指 "没有值" 或是 "未知的值"。
--WIKIpedia
这段话则说明了Null在语意上的模糊性。既可能表示“没有值”,也可能表示“未知的值”。
为什么不要返回Null
-
语意的角度
先看一段代码。这段代码使用了React 16 里面的新特性。我先不说这段代码的意思,我们尝试只看代码猜一猜这段执行这段代码以后会发生什么事情。
const MAX_PIZZAS = 20;
function addAnotherPizza(state, props) {
if (state.pizza === MAX_PIZZAS) {
return null;
}
return {
pizza: state.pizza + 1,
}
}
this.setState(addAnotherPizza);
在上面的代码里面, 我们可以知道作者定义了一个常数MAX_PIZZAS,和一个函数 addAnotherPizza,然后执行 React.Component的setState方法。
好,读方法名字的时候,我们大概可以猜测到这是一个更新披萨数量的的方法。直意过来就是“添加另一个披萨”。直接看这个函数的返回值,可以知道这个方法返回了一个对象(新的state),这个新的对象更新了pizza的数量(其实这种命名也不好,应该表明是numOfPizza)。但是我们也发现,当pizza数量达到一个上限的时候,就会返回一个null。
OK。想象一下你是一个新来这个项目的员工,你来猜测一下这里返回一个null代表了什么意思。你可能会说,那肯定就是不再添加pizza的数量咯。对一半吧。那为什么返回null而不返回空对象呢?
在揭晓正确答案之前,首先说一下什么是语意。我理解的语意就是你写的变量名,函数名,方法名,类名,接口名等等有意义。
在语言学上,指发出讯息者想要表示或传达给发现者或接收者的理念;
--wikipedia
也就是说我们编写的代码不能只有机器能够理解,以后接我们工作的同学也能轻易地理解我们的代码到底想表达什么意义。
把上面的的函数翻译成人话的话,就是:
A: 再加一个披萨 ( 函数名: addAnotherPizza)
B: 当前不足20个,好的,成功加一个披萨
...
A: 再加一个披萨
B:当前已经够20个。那么, null。
你能够理解吗?
回到上面的代码。React 16版本里面setState接收null值来明确告诉React不要渲染。所以除了不再添加一个披萨到新的state以外,还有一个意思就是告诉React不要做渲染了。
至少我作为一个新的代码维护者的角度来说的话,我不能轻易地、明确地判断出当addAnotherPizza返回null时,到底想表达什么。
-
防范NPE的角度 (NullPointerException)
有JAVA背景的同学,应该对于NPE一点也不陌生。NPE应该是伴随了我们很多的开发时间。NPE主要发生在调用一个对象的方法时,检测到应该有的对象引用的值是null。
所以为了防范NullPointerException, 我们为我们的方法和函数添加了数不清的null 检查逻辑。使用null值检查逻辑防范NullPointerException是一个很好的编程习惯。这可以增强系统的健壮性。
但是,如果你仔细观察我们的代码,可以发现很多的null检查是不必要的。特别是对于一些自建系统的接口函数调用。举个如下的列子:
class Member {
private String phoneNumber;
private String memberId;
public String getPhoneNumber(String memberId){
if ( memberId == this.memberId ) {
return this.phoneNumber;
} else {
return null
}
}
}
相信大家在很多的代码里面看见过类似的逻辑。即当成功时返回某项值,反之则返回null。
从语意的角度上说的话,你找某个管理员去询问某个member的电话号码,得到null的回馈时,你会怎么想?是去想这个member没有电话号码呢?还是没有这个member,还是这个管理员傻了?
除了语意上的模凌两可以外,在一个系统里面返回过多的null,也在一定程度上增加了null的检查逻辑代码数量,其实就是给系统增加了不必要的复杂性。所以,如果在某些情况下,比如上述返回电话号码的情况下,为何不返回一个空字符串呢?
从防范NPE的角度来看,返回空字符串的话,首先,getPhoneNumber的调用者是不需要进行null检查的。因为你不返回null,那为什么还要进行null检查?其次,主动不去返回null还有一个好处,就是我们作为一个有素养的程序员和工程师,主动从系统健壮性的角度防范了NPE的产生,从源头上保护了NPE对系统带来的危害。
不给系统留下任何健壮性危害隐患,是作为一个有素养的程序员应该有的觉悟和编程习惯。作为一个有素养的程序员,永远不要指望Debug来让我们的系统健壮,而是时时刻刻地去主动考虑如何才能保护我们的系统不因为我们技术的原因而崩塌。
-
面向对象编程的角度
在从面相对象编程的角度来展开讨论之前,我们有必要知道什么是面相对象编程。
Object-oriented programming (OOP) is a programming paradigm based on the concept of "objects", which may contain data, in the form of fields, often known as attributes; and code, in the form of procedures, often known as methods.
--wikipedia
说白一点就是在系统里一切皆对象,对象包含了一切必要的属性和属性操作方法。
那么我们的系统就是由对象和对象的关系构成的。在上面的知识回顾当中,我们可以知道null是一个指针类型的值。表示该指针不指向任何的对象。从一个系统抽象的角度来看,指针类型属于比较底层的类型。那么在我们的很多方法当中去返回一个底层的类型值是否合适呢?
举个简单的列子,我们人是由细胞组成器官然后再组成我们人这个类型的。如果每一个细胞代表了一个实例,那么每个实例所在的位置则由指针的值保存(实例内存地址)。我们人这个系统中,细胞和细胞有关系,器官与器官有关系,器官与细胞有关系。他们互相有数据的来往,有数据的处理,但是唯独没有的是,当某个器官想查询某个细胞状态时,相应器官返回一个空位置(null)的指针。因为这样既没有意义,也没有任何人体内部器官会去做这个事情。
人体是一个非常健壮和复杂的系统,我们可以通过观察我们人体自身的系统来获取编程的灵感。
null在我们的编程过程中,很多时候被误用为表明想要查找的对象不存在。如果这么想的话,只能说明你还离OOP还有一段距离。因为你还在用机器的思维在思考编写代码。如果严格按照OOP来思考如何构建我们系统的话,我们就会有更好的方法去表明一个不存在的对象(Clean Code: Special Case Pattern)。
业界相关讨论
以上都是我自己对返回null值的思考和总结。在业界上,早就有相关是否返回null值有激烈的讨论。没有讨论出业界一致公认的标准。
在Stackoverflow上关于是否返回null的讨论: Is returning null bad design?
Null之父Tony Hoare关于Null值的文章: Null References: The billion dollar mistakes
Clean Code 作者 Robert C. Martin 关于是否返回null值的看法是 “Avoid returning null”.
其他一些作者关于是否返回null的一些讨论: Why NULL is Bad?
总结
本文认为Null值不应该由程序员在方法中返回,因为这样做,一是违背了方法名的语意。二是由于我们返回null值,更加增加了系统的不稳定性;因为在调用者忘记做null检查时,就一定会因为我们的方法出错。最后,null值所充斥的系统模型违反了OOP原则。
所以,本文认为,在我们构建的系统中,不应该有返回null值的方法和函数存在。