一眨眼从事软件R&D四年有余了,对一个软件开发者来说,工作了三到四年往往就到达了他的顶峰,当然这句话不是我说的,这是我有一次到一家公司面试,那里的HR这样说的,而我不怎么同意,按照道家学说的观点,物极必反,但何为极?这是相对的,如果我在R&D领域工作了四年下来,感觉自己在这方面已经不能再发展了,于是去做别的,那这就是我的“极”了,但如果我觉得这仅仅是个开始,我还得继续学习才能取得更好的发展,那这还不是我的“极”。所以到没到“顶峰”,很大程度上取决于软件人本身的态度。这篇文章算不上严格意义上技术文章,就是一些心得分享。
一、工具篇
我用得最多的工具是VC++,准确说VC++ 6.0,有大约3年的使用经验了吧。为什么一直用6.0不用.net呢?现在VS.net 2007都出来了,而我还停留在6.0,其实我认为如果不用.net Framework的话它们区别并不大,而且6.0的运行速度快,这是关键的,用VC++图什么?速度!但由于这个,倒发生了不少争执,比如我用VC++,但别人用的是C++ Builder,虽然都是C++,但你很快就发现交流起来有多么不容易,首先工程不能互相打开工程文件,其次代码风格上差别很大,还有就是所用的库根本不同,这样基本上我写的程序,他看不懂,他写的程序,我看不懂。“高人”喜欢说:这只是工具。是啊,这只是工具,可无论怎么好的设计,最终靠什么来实现?靠工具!工具是很重要的,精通工具是有必要的。而我始终认为:开发团队尽量使用统一的工具。不为别的,为了便于交流。
VC++ 6.0(简称VC6)绝非完美,用了这么久下来真的是什么乱七八糟怪事情都遇到过了。最常出现的一个就是Link时候被卡住,这个问题在网上都成了老生常谈了,被卡的时候你唯一能做的事情就是用进程管理器结束掉它,如果你同时开了多个VC6,你经常会关错,虽然不是什么大问题,但很影响你的心情,这是公认的VC6的一个bug,但过去这么多年了,强大的MS一直没修正这个bug。也许MS力推新的Visual Studio.net,不想再管VC6了。
不知道你们注意到没有,Windows XP下,VC6的调试器无法观察到static变量的值,我两年多前发现了这个问题,但竟然很少人知道,好不容易在一个国外的论坛上找到了同样的问题描述,但没有解决方法,这又是一个bug,小问题,却带来了不少的麻烦。而Windows 2000下却是正常的,看来Windows 2000和Windows XP在内存管理上还是有些差异的,从这点看来。
但接下去这个可是大问题,VC6的IDE出现间隙性的停滞,不是编译时,而是在编辑代码的时候。很少人遇到,但我确实遇到了,最后也解决了,但即决方法是重装系统,也许这不是VC6的问题,这是Windows的问题。很郁闷。类似的还有VC6无法调试,一调试就彻底死机,遇到的人不多,但我正好又是幸运儿,又是一个束手无策的问题——重装。重装Windows,不是VC6。
提起数据库就不能不提Oracle,似乎没用过Oracle就不算懂数据库,其实我很讨厌这个观点,我认为就算自己用文本文件创建一个微型的可检索的小程序,也算数据库,只不过不算DBMS罢了。Oracle说实在这玩意儿并不适合所有人用,有时候我们所需要的就是那些效率高而又短小精悍的东西。想起安装Oracle,真实心有余悸,Windows XP的安装盘只有一张CD,而Oracle 9i就有三张,步骤十分多,加上Java写的界面响应速度奇慢无比,安装进度条就像蜗牛爬树,装一次起码得一天,装好后配置实在太繁琐了,我当时整理了密密麻麻的3页纸,交给我们的网管,叫他重复我的步骤做一遍,结果还是遇到了很多问题,对一名开发者来说,处理数据库管理的这种问题是非常不乐意的,起码我感到很烦,而对一个普通网管来说,这些事情难度又未免太高,这时候终于知道DBA的作用了。如果你现在叫我重复一遍当年这个工作,我只好认输,我现在几乎全忘了那些步骤了。说实在,Oracle不是个好用的东西,对一个中小型应用来说,它的速度未免太慢,安装未免太繁琐,而要使用它的功能的话,还得安装两百多兆的客户端,它提供的管理工具界面未免太难看,还有一些bug,令人十分难过。当然,我承认它是功能最强大的DBMS,只是这种工具未必适合我们。
Oracle提供了强大存储过程,里边可以用它的强大的PL/SQL,真的很强大,但这是我目前为止,见过的严谨度最差的语言,我最后一次使用它的时候,还是不清楚什么地方该用“;”,什么地方不该用,似乎只能通过实践来调试,才明白它的真正用法,双引号和单引号的用处也是够令人迷惑不解的。很多函数工作不正常,或者执行速度太慢,当你为此调试感到抓狂的时候,你就开始怀念友好的SQL Server了。
Access是个不错的文件型数据库,可居然不提供二进制格式的直接编辑,十分不方便。另外ADO的速度实在不敢恭维,也许它是一个公用的接口,要考虑很大的范围,企图做得包罗万象,你用ADO写一个最简单的数据库访问,调试一下,就发现问题了,它为此启动的线程竟然有6个之多,而你所做的动作不过是一个再简单不过的查询。你可以想像一下,你反复执行一个查询,打开,关闭,打开,关闭,打开……那究竟效率有多高?其实我一直很想自己写一个速度快,独立性强的微型文件数据库,用来做一些桌面程序,我们应该清楚自己的真正需要,我之后再详细说我这个观点。
二、技术细节
细节重不重要?重要,一个程序中,往往一点点小的疏漏就够你检查半天,所以程序是千万不能错的东西,但我们对细节往往不够重视。“程序能用即可”,很多开发者都是这种心态,对一些细节根本不在意。
有一次我在检查一个别人写的问题程序,这个程序很古怪,有时候正常有时候不正常,查来查去看不出什么原因,后来费尽心机才发现程序竟然有在逻辑处理线程中对MessageBox进行调用,开发人员不懂得这个细节,一遇到这个MessageBox,逻辑过程就受到影响,MessageBox是很好用,但这是不能乱用的,即便在界面线程中,最好也要指明MessageBox的父窗口。还是那位兄台写的程序,一旦出现点什么问题,能把MessageBox弹得满屏幕都是,而且每弹一次任务栏上就出现一个图标按钮,想关掉程序都关不掉。
不知道你们对编译时产生的Warning是否很在意,我是很在意的。比如把有符号数赋值给无符号数,符号就丢失了,一个无符号的整型上限大约是4亿,而一个有符号的整型上限大约只有两亿,这都是要注意的地方。几年前的事情了,我在玩一个网络游戏,私服的,杀怪掉钱特别多的那种,杀一个怪就可以捡好几百万,当我捡到21亿的时候,钱竟然变成负数了,什么东西都买不了,郁闷啊,其实写代码的人都非常清楚了,用来保存玩家的钱的变量是个32位的long型,这也不能怪程序设计者,他们当时设计这个游戏的时候根本没想到过玩家能打到这么多钱,普通情况下一个怪才掉几百。
有一次我问一个朋友,像“char *p = new char[10];”这样的语句什么时候会导致出错?他回答:“内存不够的时候。”但现在的电脑动辄上G的内存,再加上windows还有虚拟内存机制,怎么可能会出现“内存不够”?而且“内存不够”会导致的是分配失败p==NULL,而不会出错。我并不是无聊拿这位朋友开刷,这个问题我确实遇到了,在heap分配时候出错,而调试时看到的却是一大堆汇编代码,我百思不得其解,但最后我发现了还是我的问题,(这不是废话么?)我们不能责怪编译器,只能从自己身上找原因,我经过细心检查发现程序中有这么个用法:
void GetTempNameWithPath(_TCHAR *szTmpFile)
{
_TCHAR szTempName[MAX_PATH];
memset(szTempName, 0x00, sizeof(szTempName));
::GetTempPath(sizeof(szTempName), szTempName);
_tcscpy(&szTempName[_tcslen(szTempName)], szTmpFile);
_tcscpy(szTmpFile, szTempName);
}
这个有错吗?我写的,当时我也认为没错,GetTempPath这个函数第一个参数是长度,第二个参数是用来存储获取内容的字符串指针,这应该没什么问题吧。错误出在后面一句,就是第一个_tcscpy操作时候出错了,执行这个GetTempPath前,通过参数传入的szTmpFile还是有值的,执行完之后,就变成NULL了,所以一到_tcscpy就出现内存访问非法错误,但GetTempPath怎么会对szTmpFile有影响呢?我检查了szTmpFile和szTempName指向的地址,这两个地址相距有超过520个字节,按理说怎么都不会影响到啊,你们知道为什么吗?错在GetTempPath的第一个参数,看MSDN上是怎么描述的:“[in] Specifies the size, in TCHARs, of the string buffer identified by lpBuffer.”size in TCHARs,呵呵,而我用的是sizeof运算符,sizeof运算符返回的是对象的字节长度,而不是size in TCHARs,所以这个程序在使用_UNICODE编译选项的时候就出错了,这个时候的sizeof(szTempName)的值是MAX_PATH*2,是520,GetTempPath却把这520当作是520个TCHAR,所以它操作的范围一共有520*2=1040个字节之多,越界了,导致后面不该访问的地方被访问到了,所以szTmpFile的值才会被莫名其妙地修改。那又为什么导致内存分配错误呢?我只能笼统地说:访问越界,一切错误皆有可能。没有越界检查,这也是C/C++最为人诟病之处,所以我们写程序时候得处处提防。
前面也提到了,如果不使用_UNICODE,是不会发觉这个错误的,程序也就可以永远正确运行下去,但正好我们公司的产品要发往世界各地,要适应多语言,所以只能使用UNICODE,关于UNICODE,我前面有一篇文章提及到,这篇文章不是专门讲UNICODE,但大家可以参考参考:http://blog.csdn.net/guogangj/archive/2007/07/12/1686236.aspx
现在还是继续讲一下国际化,提起国际化大家可能都觉得没什么,统一用UNICODE不就行了么?可实际操作起来可不是那么回事,比如你能说出_TCHAR和TCHAR的差别吗?当然,你看了上面那篇文章的话你是肯定能说出的了。还有各种支持UNICODE的函数你是否都精通,不会出现我前面提到的那个GetTempPath这样的错误?如果这一切你都OK了,那接下去的问题恐怕没多少人能解答。
我一向是一个很敬业的人,写的程序也尽可能让它完美,比如让它有支持UNICODE和MBCS的两种版本,有一次我写了个程序发往美国去用,他们反馈回来的信息就是显示出来的字符全是黑方框,还截了图,我觉得很奇怪,我已经使用了UNICODE啊?怎么会出现这种问题?况且就算我没用UNICODE,我的程序上边全是英文,没一个别的语言的字符,没有道理显示不出文字的,在我的环境下没办法重现这个错误,我把问题发到csdn.net上也无人能回答。但有一点得肯定的是,我们没有任何理由要求美国那边安装中文支持来测试我们的程序。后来,那边的IT解决了这个问题,估计是安装了中文支持,但这个错误到现在我还是无法改正,也就是它还一直潜在我们的软件中,我至今也不明白为什么,因为我的电脑上也有纯英文版的Windows(没有安装任何其它语言的支持),却没发现这个问题。记得以前一位IT人跟我说过,有些问题一辈子只会碰到一次。
下面来讨论一个还是“国际化”的问题,是这样的,你需要创建一种文件格式,这种文件格式包括了一些字符串数据,比如:”我是字符串ABCabc123”,那这种字符串如何保存在文件中,也许你想都不想,既然让程序存在两种版本,那就以_TEXT()方式就好了,该MBCS的就MBCS,该UNICODE的就UNICODE,于是写到文件去的字符串就可能存在不同的格式,可能是MBCS的格式,也可能是UNICODE的格式,数据明显是不同的,这意味着什么?用MBCS创建的文件,只能由MBCS的程序打开,反之亦然。那我们发布程序的时候究竟是UNICODE还是MBCS?答案是都有可能,这往往是临时的决定,因为公司没有规定。如果客户拿到了UNICODE格式的文件和MBCS的程序,那使用过程中出现问题,这几乎是肯定的,所以文件格式必须得统一,要不都用UNICODE,要不都用MBCS,我是觉得MBCS的好一些,毕竟长度会短一些。那假设就使用MBCS吧,于是UNICODE的程序读入文件的时候就得经过转换,还记得是哪个转换函数么?对,就是那特别特别繁琐的,MultiByteToWideChar,而对于本来就是MBCS的程序,就不使用这个转换,你得在程序中处处注意。怎么样?感觉“国际化”没有你想像中的简单吧。
还有一次,我们把一个认为已经很“成熟”的程序送去美国,他们报告就是用不了,出错的截图也回来了,分析来分析去我们认为是访问的数据库(我们使用的是Access的文件型数据库,mdb格式的)不存在,或者没法访问。可程序在我们这边是根本不会出现这种问题的啊?于是我们把微软的Jet引擎打包到程序中去,让他们安装,确保不是因为引擎的问题,但错误依旧。而且看着这个臃肿的程序包,自己心里真难过,因为实际有用的程序一MB都不到,而那个Microsoft的引擎四五MB那么多。那我们也不能光从代码上找原因啊,毕竟程序在我们这边尝试是没有问题的,那这个错误如何重现?我相信是由于语言版本的问题引起,于是花了九牛二虎之力安装了一个“纯英文版”Windows,也就是上述的不带任何其它语言支持的Windows,终于找到了问题,原来我们的mdb文件是使用中文版的Access来创建的,在没有中文支持的情况下,mdb内部的次序可能会带来问题,MSDN上已经提到了这点,我只好在纯英文版下安装一个纯英文版的Access,然后重建MDB,修正了这个错误。通过这点来看,包括微软,在“国际化”面前尚有些感到棘手,不能完全解决兼容性问题;还有就是我们能输入中文的英文版Windows其实都不是真正意义上的英文版Windows,只要安装了别的语言支持就算不上英文版Windows,如果真的要测试出些问题,就得学学我,安装一个“纯英文版”Windows。
Windows比你想像中的要复杂得多,记得以前哪位院士说只要有充足的资金就能开发出一个Windows Like的OS,我原来觉得他有些夸张,现在觉得他是在痴人说梦,这样说的人都是不懂Windows的人,甚至都是不懂程序的人。既然我们可以断言中国10年内开发不出《魔兽世界》,那我们就可以断言中国永远开发不出Windows,永远不会出现微软,即使政府介入,天才要具备天才成长的土壤嘛……这是题外话,我只是觉得要完全掌握Windows的内部机制是几乎不可能的,我对我直接用WinAPI写出来的又小又快的程序感到骄傲,当对比用MASM直接生成的代码,就逊色得多了,那还有一些不经过WinAPI,直接对硬件进行操作的代码,那更令我汗颜,去看看那些仅仅几十K却能完成强大功能的黑客工具,了解一下什么叫编程的艺术。
三、管理篇
“安全三分靠技术,七分靠管理。”——某人
现在看来,我所遇到的大多数问题并不是什么严格意义上的技术问题,而是管理上的疏漏。先举个例子吧:
有次上级对我提出了一个修改要求,这要求并没什么难度,我很快就完成了,提交了我的程序,可他反馈回来说没有改,还是老样子,还附带了运行截图,我感到奇怪,于是确认,又重发了一遍,还是说我没改,我纳闷了,检查了一遍邮件系统,存放位置是否正确,再自己跑了一遍程序,瞪大眼睛对照了一次,发送,还是有问题,程序没改!这到底怎么回事?你知道问题出哪里么?我的程序是改了,没错,发送也没错,他接收了,安装了,都没错,错就错在运行,他是点桌面上的图标运行程序的,而我这个安装包根本没有创建桌面图标,也就是说他运行的还是老的程序。这个很难说是谁的责任,因为安装包如何制作,公司没有明确要求,是否创建桌面图标这个勾默认是选,抑或不选,这也是没有要求,所以就出现了这样的问题。
我们平时可能面对得最多的问题就是版本问题,哪天市场部的人来找你说,软件不正确,你又检查不出原因,最后发现他们用的是旧版本的程序,而你自己也在想啊,为什么不把新版本的给他们呢?这就是版本上的混乱。什么会导致混乱?举个例子:公司开发了一台仪器A,你写了一套对应的SoftA程序,但很快客户提出了一些改动,把仪器A改为A2,但依旧保留A,这只是个临时更改,于是你的程序也要跟着改,改为SoftA2,如果你不了解这只是个临时改动的话你就会直接在原有基础SoftA上修改,把以前的SoftA给覆盖掉了,于是仪器A发布的时候,你可能附带的是SoftA2程序,这样就出问题了。我发现就是由于针对不同客户的这种细微的改动最容易产生版本混乱问题。而你很自信:“我是不可能搞错的。”可能只有少数两三个程序的时候你不会,但程序稍微再多一些就不一定了,还有就是时间,人的记忆力总是有限的,时间一长就容易淡忘,某天市场部的人来找你说半年前的一个程序现在出了问题,你一脸惊愕:“什么?半年?我找找看。”一找就是半天,还不确定那个版本是不是还真的健在。
所以,我们得用版本管理工具,以前用CVS,现在更流行SVN,其实我也不清楚两者具体差别,听说都同一作者,配合起TortoiseSVN,真的挺不错,我们可以弄一张表,xls就行,放在网上一个共享文件夹中,这张表用来说明那些人负责那些容器(repository),这些容器存放在什么svn地址,再加上注释,这样就差不多了,对一个小型团队来说。
我这些年工作下来,有一个发现,不知道算不算新发现,那就是:一开始就企图把软件做得大而全的话,结果总是很凄惨的。中国人的一个特点就是喜功好大,有了一点成功,就喜欢幻想未来如何如何,其实还是脚踏实地好。我考虑了很多软件,比如Windows,从1985年的Windows 1.0诞生到现在,经历了多长时间?Windows Vista不是一两年就搞起来的,即使是像微软这种公司,有着世界顶级的开发人员,也需要时间去完成技术的积累。我曾经做过一个项目,一开始说是一个文件分析系统,它的功能被描述得很简单,就是扫描整个磁盘,把文件归类。听起来简单,其实已经不太简单了,起码我能马上想到两个难点:1、这么多的文件类型,难道叫我一一识别么?2、分类好之后我需要拷贝这些文件到特定的位置,哪来那么大的磁盘空间?这只是粗略一想,就知道有这两大问题。而提出这个项目的人可不会考虑那么多。如果再进一步细想,还会发现很多很多问题,比如:系统目录下很多文件是不可访问的;IE的缓存里很多文件是无用的,难道也要拿来归类?IE缓存里很多文件是临时文件,名字是不规则的,也没法访问,这个怎么办?如何识别文本文件这种没有特定格式的文件?好了,如果这些都不足以难倒你,那接下去他们提的需求应该能让你惊叹:1、要脱机分析出注册表文件中的内容,比如地址栏历史记录,隐藏在注册表中的密码,安装的软件信息,网络设置等;2、把上网记录打印出来,并从IE缓存中分析出用户WEB邮件的内容;3、对Word,Excel等文件需要作加密判定,以便破解;4、尝试破解Windows密码;5、找出用户所有的联系人;6、保存用户所有的Email(比如Outlook中的)为eml格式;7、分析出用户QQ,MSN的聊天记录;8、“敏感文件”分类存放;9、生成报表……(对了,别对提出这个需求的人进行过多的揣测,我不能透露半点关于他的信息,呵呵)Windows 2000的注册表数据库不知道你们有了解不?是一种特定的格式叫Hive(蜂窝),当然这种格式是不开放的,微软的注册表编辑器能使用这种格式,但叫我们自己写一个还真不容易,所幸的是我发现了一个开源软件正好有这个功能,但提取密码就难了,各种密码的存放方式都是不同的,微软有一些不公开的接口可以做到这点,但“脱机”状态下如何使用?你们知道“上网记录”是存放在什么地方么?并不是注册表,而是IE缓存中的一个Index文件,好,又要开始自定义文件分析了,更糟糕的是对不同的IE,这个文件的格式还有可能不同……OK,后面的我不说你们也知道难度了,结果你猜如何?我完成了上面的绝大多数功能,并将这些都整合到我的软件中去,就我一个人,现在想想我实在太伟大了。但请问这个软件叫什么名字?还叫文件分析系统么?而当我完成这个“怪物”提交之后,就很少收到使用反馈,我想用户对这么一个怪物也无所适从,诸多的功能整合在一起,有些不伦不类,而且需求一开始就提得有问题,后面怎么做都是错的,但这不是我的错。类似这样的事情还有,这里就不一一列举了,这得归咎于管理上的错误,而不是什么技术问题。
我还认为,不要相信CMM,那东西对大多数公司来说,根本行不通,CMM往往会使得其本身成为了目的,最后不善而终,有人还以为微软是典型的CMM5公司,我倒想知道这消息哪来的,要是说IBM是,我还相信,微软才不屑搞这个东西,我在以前的一份工作中,竟然有幸进入过CMM的队伍,那个叫混乱……我认为:企图把软件开发当作一种类似工厂流水线的重复劳动本身,就是错误!
OK,大的就不说了,就说个小事情,领导喜欢出自于某种目的,要求员工写“日记”或者“周记”,他这样来提:“每天都写一小段,来向我反映你的工作情况,写多写少都没事,让我知道你在做什么事。”我呆过的5家公司中有4家这样要求过,结果都以失败而告终,为什么?员工根本不乐意干这个无意义的事情,举个例子,有段时间我在开发一个叫“斗地主”的游戏,于是我在日记上写道:“斗地主;斗地主;斗地主……”一路走过来,我多少明白了些管理之道。
四、用户篇
用户跟我们的最大区别是他们不懂技术还非常想当然。所以很多看起来没问题的程序,到了用户那里就出现问题,当然这不是他们的错,只能说我们的程序鲁棒性太差。
有次我开发了一套场地游戏,就是在一个局域网中进行游戏的系统,有一个服务器,若干个客户机,我们本地测试都一直没发现什么问题,到了真实场地,问题频频,我百思不得其解,只好亲临场地看个究竟,不看不知道,一看吓一跳,所有乱七八糟奇怪现象都出现了。场地环境比较恶劣,电脑是配置非常差,网络时通时断,最糟糕的是他们的关机方式:直接拉闸。我只好跟场地的人员说:“电脑,不是这样关D!”当然,后来我意识到这种情况后,也对程序进行了大量改进,来使得在更恶劣的情况下也应该能适应。
先写那么多吧,这些问题远远不是全部,我觉得很难把所有的问题都整理起来,所以想到什么就写什么。在自己不太忙的时候。