【IT168技术评论】几日前在Cafe午餐的时候,大家聊起一些在Windows操作系统源代码库中曾经看到过的一些趣闻逸事,比如那个著名的“because Exchange is a moron”(正好这天公司的Exchange服务器巨慢,所以大家更是大发一笑)的注释。这其中有人提到Windows代码中大量使用goto语句的这个事,这让我想起这样一个有趣的问题:
在程序代码中,我们为什么使用goto,或者,我们为什么不该使用goto呢?
我曾经不止一次地听某某义正言辞地向我宣传goto是邪恶的,但如果我追问这么说的理由为何时,通常的答案都是模模糊糊的人云亦云之类的回答。大部分的理由都会指出goto破坏了程序的可读性和可维护性,如果代码里到处都是goto来goto去,到最后谁都很难搞清程序goto到哪一个地方了。
这看似颇有道理的说辞其实充满了迂腐的书生气。稍微有点常识的程序员,难道真会如此到处使用goto么?显然不会。如果说真的有那么一位程序员是到处在用goto把他的程序逻辑拼接起来的话,那我想他不是天才(汇编写太多了,到处都要自己跳转)就是无知(完全无法结构化自己的算法思路)。而软件开发作为一个工程行业经过这么多年的发展,现实中已经很少会真的有这种滥用goto的现象了。这当然也要感谢于那些关于goto邪恶性的大力宣传,大家上procedural programming第一课开始,就被反复灌输了“不要用goto,不要用goto”的观念。
那为什么Windows操作系统代码中大量使用了goto?是不是微软总部都雇佣了些烂人,大家都在混饭吃?还是说对于goto的使用是其实很有选择性的?而从当年goto的大量出现到今天这个关键词在使用C#或Java写就的程序中几乎绝迹,这一切,其实都是有其历史背景和含义的?
要回答这些问题,我们首先讨论一下goto在Windows操作系统源码中的使用。如果仔细观察一下的话,你会发现goto的使用其实都是在一种很特定的场合,那就是:系统资源的回收和释放。这里,系统资源可能是一块字符串内存,可能是某个内核对象(比如event或mutex)的句柄(handle),也可能是更复杂一些的数据结构。所以,goto出现的代码段,通常有这样的结构:
... Magic::Initialize();
BSTR someString = ::SysAllocString(L"Some random string");
hr = CallSomeAPI();
if (FAILED(hr))
goto EXIT; ...
hr = CallSomeOtherAPI(); if (FAILED(hr))
goto EXIT;
...
EXIT: Magic::Uninitialize();
::SysFreeString(someString);
...
}
如此便不难理解为什么goto在这种特定情况下可以简化代码编写的结构,使之更清晰易懂了。试想如果不试用goto,我们的代码就会变成:
...
Magic::Initialize();
BSTR someString = ::SysAllocString(L"Some random string");
hr = CallSomeAPI();
if (FAILED(hr))
{ Magic::Uninitialize();
::SysFreeString(someString);
return hr;
}
... hr = CallSomeOtherAPI();
if (FAILED(hr))
{
Magic::Uninitialize();
::SysFreeString(someString);
return hr;
}
... return S_OK;
}
要做回收处理的资源越多,这样的写法就显得越冗长,因此goto在这里是很自然的一种选择。
但随着面向对象的编程模式(Object-Oriented Programming Paradigm)逐渐地开始取代过程式编程(Procedural Programming),程序员开始发现有一种更好的模式(Pattern)可以用来取代goto,那就是RAII(Resource Acquisition Is Initialization)模式(“资源分配与初始化同步”)。RAII的主要思想在于两点:1. 对象在且一定在被分配或构造(construct)的时候同时被初始化,这样就避免了资源在没有被适当初始化前就被用户调用。2. 对象在被析构(destruct)的时候释放所占有的资源,这样就防止了资源泄漏。这个模式最为大家所熟知的应用可能就是C++标准库或者COM编程中随处可见的“聪明指针”(smart pointer)了。比如在上面的例子中,我们就可以定义一个MagicPtr的类,然后在类的构造函数里做Initialize,在析构函数里做Uninitialize。而对于BSTR,微软已经提供了相应的类了,那就是_bstr_t
利用goto来释放资源在procedural programming的时代是一个自然的选择,所以在Windows的源代码中你会看到goto的踪影,因为Windows在OO思想大行其道之前就已经存在多年了。但随着OOP的深入人心,遵循RAII来管理资源就成为了最自然的选择。
另一个重要的原因,就是异常处理(exception handling)概念的兴起。goto虽然可以很干净地解决过程式资源回收的问题,但却对异常这个东东没有很好的解决方法。比如上面的程序要是哪里抛出一个异常的话,那goto的部分就根本不会被执行了。而另一方面,RAII却能很好地解决这个问题,因为在对象离开定义域之前(不管是return了还是exception thrown了),析构函数都会被执行的。
其实写这篇东西的另一个目的也是想说:每一件看似简单的事情背后,如果你花一些时间去思考和研究,也许就会发现很多更深刻的意义和结果。这并不是要我们变成一个多疑的偏执狂,而是我觉得思索和提问的习惯是有益的。对于一个看似简单的道理,我们能不能提出让自己信服的佐证来,我们是否有一种直觉,告诉自己:I am wondering if there is more to it。事实上,这个世界上的偏执狂是少数,多的,是人云亦云的大众。