2. 正确的使用断言与返回
有一天的清晨,老A早早的来到了办公室,决定对团队的成员近期的代码进行一次code review(代码审查)。当然,审查对象是入职不久的小C。小C打开svn,展示了一段最近写的代码。
function processItem(){
assert(gItem) //断言道具表存在
assert(gEquip) //断言装备表存在
//其他代码
}
function processItemEquip(){
if (! gItem) {
return
}
if (! gEquip) {
return
}
//其他代码
}
这2个函数的意图很明显都是在函数入口处对配置不存在的时候进行的处理。那么我们首先应该清楚在逻辑上,这2个配置表可能是为null的,也就是空的。当配置表的值为空的时候,我们去访问它的内容就会报错,所以我们需要去处理这个情况。那么我们究竟应该选择哪一种写法呢?
先讨论一个问题,什么时候应该用assert进行断言,什么时候应该用return。
a. 在逻辑上面允许发生的情况下,我们应该用return。
b. 如果是逻辑上认为不应该发生的,我们应该用断言。
在上面的问题上,如果在调用processItemEquip可能存在gItem为空的情况,那么我们在这边应该用return。如果在设计上,processItem调用的时候gItem应该存在了,那么应该用assert。我们不希望我们在使用一个配置表的时候都需要去判断它是否为空。所以当我们应该把逻辑设计成在一个函数调用某个配置的时候,这个配置就是存在的,这样会大大减轻后续代码的各种判断。如果配置在访问的时候就应该是存在的,那么它一定不会是空的。如果它是空的,那么一定是我们的设计出现问题。这时候我们要大胆使用assert,在第一次出现问题的时候及早中断执行流程,立刻排查错误。
讨论下面一个场景, 写一个引用计数的2个接口
this.nRef = 0;
function incRef(){
this.nRef = this.nRef + 1
}
下面是2种写引用计数的写法,哪种更正确?
function decRef(){
this.nRef = this.nRef – 1
assert(this.nRef >= 0)
}
function decRef(){
if(this.nRef <= 0){
return;
}
this.nRef = this.nRef – 1
}
第一种是断言,第二种是容错。
小C觉得使用第二种比较好,这样就没有必要关注外界用错导致引用计数被多减的情况,方便了外界逻辑层的调用。那么为什么我们还是推崇第一种写法,因为第一种写法是在问题发生的时候第一时间把错误断住了,它要求逻辑调用者必须找到引发断言的根本性问题。
那么我们经常在什么时候会写return这种兼容的代码?比如说一个网络消息过来,检测道具数量够不够,第一次协议过来是够的,第二次协议过来就不够了。那么我们在检测道具不足的时候用return而不是assert。那你可能会说客户端也加检测就可以assert了。考虑到第一条协议可能还没返回新的数据给客户端的时候第二条协议也过来(数据层我们统一以服务器为准),这时候客户端是拦不住的,所以这种情况我们使用return。