问题引入
在数据层各个模块之间的调用,比如A调用B,而B需要去拿C的数据做一些逻辑判断,如果C的数据为空就会报出空指针异常,那么问题来了,B“拿”到C数据需不需要做数据检查?
一种回答是:既然都知道空指针异常了,肯定得做数据检查啊。
首先可以肯定是必须数据检查的,但仔细想想,问题就来了,在这写数据检查只不过是一种打补丁方式,这种调用情况在项目中随处可见,那么出现B拿C数据的情况就一定要检查吗?就这个问题总结了一下。
空指针异常,很多都发生在使用数据时,而这时候问题就出现了,是不是每个使用数据的地方都需要做空值判断呢,这样的防御式编程到底有没有好处,当然你可以觉得这样做可以有效保障的程序运行的出错几率,同时随着开发的迭代,模块越来复杂,数据越来越复杂,这样的数据有效性判断遍布程序的每个角落,相信有代码洁癖的人也都看不下去了。那么问题来了,怎么来解决空指针异常,并且又不要每个地方做这些无聊的判断呢。
防御式编程首先讲到异常处理的几种方式,断言和异常捕获。
数据检查
通过自己写if-else判断数据,if - 做什么,else if - 做什么, …… ,else 做什么(为了程序的健壮性考虑,所有逻辑判断应该都要有一个else,在switch应该也要有default)
断言
判断一个布尔表达式,如果true程序通过,如果false,根据不同语言对断言的实现,一般会弹出断言对话框(这是告诉程序员),这里有一个断言啊。断言一般用于处理函数“内部”逻辑。在C++里面会断点到断言处。
try-catch
语言提供的异常捕获机制,可以简化普通的数据检查,不管有多少个if-else if条件,只需要把逻辑代码都放在try中,这样那段代码无论发生什么异常都会直接catch,而不会阻断程序运行。但是同时如果真有多个条件时,捕获到异常还得去找具体是哪个异常。
目前项目中没有统一的好的异常处理方式,开发者根据个人的喜好,在开发阶段怎么处理异常,或者使用阶段怎么避免异常,以上三种方式是比较常用的。
借鉴cocos2d的代码,里面很多do-while(0)配合参数检查的形式,有效的避免了异常的蔓延;同时定义了很多参数判断的宏。
“边界”问题
为什么会提到边界问题?其实很多异常都是“边界”造成的,边界检查是和异常处理直接联系的,主要是搞清楚什么时候去检查边界,注意两点:
- 参数边界,当函数有参数输入时,最好在函数内部有输入参数检查;
- 函数提供给“外部”使用,需要参数检查,防止错误数据进入到子程序内部,造成不可预期的错误,当在一个类或某个特定范围内部使用的话(如私有函数使用内部数据做逻辑处理),这时可以不做检查;
关于异常检查注意几点
1、处理好“边界”问题。
2、处理好游戏的状态,这里的状态不光是ai、还有游戏运行的状态、界面的状态,所有包含状态的地方。从逻辑上分清楚,哪些状态下,该做哪些事情,哪些状态下,哪些数据是一定存在的。例如:当游戏登出的时候,角色数据已经清除了,在登出状态下一切使用角色数据的操作都不应该调用,如果使用必将报错。
3、在设计函数时,应该注意:只要外部传入的参数正确,就应该给返回正确的值(对于有返回值的函数)或是正常运行。如果出现错就是函数内部该解决的问题;如果是外部传入参数不符,则应该外部解决问题(为什么传入参数不正确)。
4、数据源(业务数据层)保证数据在“某个状态”下数据的完整性和正确性。(业务数据层各个模块之间调用,在某个状态下,某些数据就一定是正确的,是不需要参数检查的,如果数据报错,就应该去找数据错误原因,是没设置好?还是被修改了?也正是这种情况下没有在上层逻辑做检查,才能使业务数据层数据错误的bug展现出来,不然由于这里的容错,会引发不可预期的bug,然而一路找下来,发现还是在源头出错了,绕了一大圈得不偿失)
异常检查的原则
1、把异常在小范围内解决,防止蔓延。
2、异常处理是为了让程序更稳定,而过多的异常检查会使代码臃肿,在两者之间找到一个平衡点,哪些检查or不需要检查。
3、异常抽象层次一致性。
防御式编程
有人把防御式编程说成是防御式编程模式,叫法不一,本质一样。这里就不深究防御式编程的理论了,大概意思是不要相信传入参数,一定要做参数检查,避免错误参数进入子程序内部,导致一系列不可预期的错误。
一定不要“垃圾进,垃圾出”,一定要拴住垃圾的输入,不能让它扩散!用错误处理程序,用断言,用异常处理,拦截住垃圾输入,并给出好的处理方案;
处理不严重的错误时,直接用错误处理程序;处理严重错误时(可能需要终止程序)就用断言和异常,但千万不要滥用。
以上是理论的东西,但是要在项目中灵活运用也不是那么简单,比如怎么处理状态这个问题就很复杂,有人中用状态机?用行为树?反正要应用起来挺麻烦的。总结了这些,以后在写代码的多考虑一下。