编程中的防御式技巧

May 13, 2015


现代软件开发过程中,遇到各种由于代码编写不当带来的错误,如何提高代码的健壮性是串通开发始终的命题。现代软件工程中倡导防御式编程,何谓防御式编程,说通了就是在将可能出现的错误造成的影响控制在有限的范围内,提高软件的健壮性。防御即为抵挡,将可能出现的错误扼杀在摇篮里或完全控制错误造成的影响,将影响减至最小。

以异常处理为例,我们对于异常一般的处理如下:

try
{
	// 想干嘛干嘛		
}
catch(Exception& e)
{
	// 善后处理
}

一旦try语句中出现任何异常,程序将会跳转至catch子句中,我们在catch子句中做代码上的善后处理,比如资源释放。

// 1.
delete p;	// a
p = nullptr;	// b

// 2.
try
{
	delete p;	// c
	p = nullptr;
}
catch(Exception& e)
{
	// 善后处理
}

如果p已经被delete过,那么a代码将会出现内存访问异常,如不出意外,程序会abort。这在某些情况下是难以接受的,用户或许正在操作某种数据,一旦abort意味着用户数据将会丢失,损失是惨重的。有了try…catch语句之后,程序abort之前会有一个专门的地方留给我们做善后操作,资源施放、数据落盘等。

说明:实际上对于p还有更好的处理。

在C++代码中,防御式的编程有更明显的体现,在很多开源库中都会见到不少的do…while的写法,cocos2d-x中对于do…while的使用非常多。do…while写法在防御式编程中也是一个常用的手段。

bool Fuck()
{
	bool bRet = false;
	int *pAssHole = new int;

	bRet = fuck1();
	if(!bRet) 
	{	
		delete pAssHole;
		return bRet;
	}

	bRet = fuck2();
	if(!bRet) 
	{	
		delete pAssHole;
		return bRet;
	}

	bRet = fuck3();
	if(!bRet) 
	{	
		delete pAssHole;
		return bRet;
	}
}

上面的代码冗余,非常不灵活,这样的代码往往隐藏着错误。如果我们采用do…while的写法:

bool Fuck()
{
	bool bRet = false;
	int *pAssHole = new int;

	do
	{
		bRet = fuck1();
		if(!bRet) 
			break;
	
		bRet = fuck2();
		if(!bRet) 
			break;
	
		bRet = fuck3();
		if(!bRet) 
			break;
	}while(0)

	delete pAssHole;
}

我们明白do…while(0)肯定会执行一次,一旦某个if不成立,只需break跳出做其他处理即可。很大程序上减少代码冗余,提高程序健壮性。

Sprite* sprite = new Sprite;
CC_SAFE_DELETE(sprite);

来看看CC_SAFE_DELETE的实现:

#define CC_SAFE_DELETE(p) do { if(p) { delete (p); (p) = 0; } } while(0)

也是采用了do…while手法。但是这种手法因人而已,总的来说,它是非常好的手段。这里的do…while有啥意义呢?

// a.
#define SAFE_DELETE(p) delete p; p = NULL;
if(nullptr != p) 
	SAFE_DELETE(p)
else
	// 想干嘛干嘛

// b.
#define SAFE_DELETE(p) {delete p; p = NULL;}
if(nullptr != p) 
	SAFE_DELETE(p);
else
	// 想干嘛干嘛

如果不使用用do…while,那么显然a代码无法通过编译,因为else无对应的if语句。b代码同样无法通过编译(加分号的习惯很严重:))。

当然,每个block之前强制加上{}可以解决这个问题,这就是仁者见仁智者见智的问题了。

话题回到Lua里来,脚本语言很灵活,但这种灵活的代价是易出错。游戏中Lua脚本的存在感非常强,归于其简单易扩展弹性高。

-- test.lua
GlobalValue = nil;
function SetGlobalValue(data)
	GlobalValue = data;		
end

function DoOtherthings()
	local times = GlobalValue.Times;
end

上面的代码,看起来是没有问题的,代码可以跑的不错,但不是最佳。游戏中一般都需要热更新功能,以便随时可以在不停机的情况下解决错误。

一旦热更新之后,上述的代码将会有问题。如果SetGlobalValue在主程序的生命周期内只会调用一次,那么热更新之后势必要重新解释一遍test.lua文件,那么问题就来了,GlobalValue将会为nil值。接下来的逻辑如果没有判断GlobalValue是否为nil值,将会导致错误出现。正确的代码应该是:

-- test.lua
GlobalValue = GlobalValue or nil;

再来聊聊经常用的for:

for k, v in pairs(task) do
	-- 该干嘛干嘛	
end

如果task为nil值呢?防御式编程的目的是将错误的影响减小,假设这里task为nil值是可允许出现的情况,那么for会在task为nil值的情况下出错,我们应该将影响减小至最低:

for k, v in pairs(task or {}) do
	-- 该干嘛干嘛	
end

在强类型语言(C++为例)中,NULL有时候被定义成0,那么你可以这样:

int* p = NULL;
if(!p)
{}

int i = 0;
if(!i)
{}

这两者在某种情况下是等价的,但在Lua中不尽如此。在Lua的if中,nil与false等价,你这么写:

-- a.
if not false then
end

if not nil then
end

-- b.
if not nil then
end

if not 0 then
end

上面a代码的效果一致,但b代码不尽如此。原因是因为nil与0不等价。所以对于nil与0的判断应该谨慎,别忘记了,防御的目的是扼杀错误和减小错误带来的影响。