程序员的七种武器
世界上有两件事情,需要一辈子的努力。第一是学习,第二是锻炼。其实这本是说明一个问题:每个人在提高自己能力这件事情上,需要持续不断地努力。以最典型的例子来看,只有通过学习,程序员才能保证不断进步。一方面我们学习新的软件技术和更新速度极快的业界新名词;另外一方面,我们也需要不断加强基本技能的巩固。
在这样的前提下,“程序员的七种武器”这个选题就应运而生了。撇开众多软件开发人员的基本素质(学习能力、解决问题能力等)不谈,我们希望能够通过本期专题来让读者更加清晰地认识,如何才能成为一位具备基本技能的开发人员。
经过几轮不断的讨论,若干专家和编辑谨慎地选择了七种程序员的基本技能,它们是:
· 数组、字符串与哈希表
· 正则表达式
· 调试
· 两门语言
· 一个开发环境
· SQL语言
· 编写软件的思想
作为一位开发人员,可能对上述所有的技术都嗤之以鼻——这些都是最根本的软件开发技术,何以被称之为武器?然而,正是这些最根本的东西,成为了很多软件开发人员向更高层次迈进的绊脚石。在多次的沟通和讨论当中,许多软件技术专家感同身受。
归根结底,程序员的武器就是为了让他们更好地完成工作,并将多年来的经验与知识融入到实践当中,让计算机为人们提供更多、更好的服务。而程序员的武器,同样是他们了解计算机和软件到底是怎样一回事的途径,希望《程序员》的读者通过这个策划,重新认识软件开发的根基,重新认识程序员的职业。
程序员的武器大家谈
《程序员》:您认为什么样的程序员才算是优秀的程序员或是程序高手?
雷军:优秀的程序员,其实并不在于技能的熟练掌握,而是需要有良好的素质,有追求完美的精神。真正的好程序员不是为了完成任务,也不是为了交付产品而工作。他们想要要发挥自己的极限,尽自己的最大努力把程序做得更好。
很多人把写程序看成是一种生活技能,而不是真心喜欢。这种后果会导致很难坚持。一些开发人员在面临选择的时候常常会选择眼前利益以及轻松的工作。但总体上来看,做一个程序员能吃苦是必要条件之一。
今天程序员要像大长今那样,用纯洁、认真的心去做好每一件事情。其实长远来看,程序员写好代码,其他收获也会纷至沓来。一些人很在意眼前的利益,但对于做开发这件需要长期积累的事情来说,看重眼前利益很容易影响其开发功力的修为。这里用大长今来对比,其故事讲的是:一个古代的科研工作者,为了研究膳食和医道的极致而不断追求。这种精神应该是我们当代技术人员应该具备的特点。
如今,金山有七八百人的研发队伍,在我们当中的佼佼者,基本上都具备上述基本素质,这也是我为什么会谈到这些条件的原因。
《程序员》:哪些技能是一个程序员必备的基本技能?
雷军:从技术人员的角度来看,我觉得最重要的是数据结构,它最能体现开发人员的基本素质。
首先是标识名的命名习惯。标识名命名习惯决定程序后期的可维护性。现在我们看到很多参加奥林匹克竞赛的程序员,常常能非常快速的写程序,但是做出来的软件很难维护,这就无法应用到实践的工程和项目当中。
第二是数据结构的定义和常量的定义,这两者对于开发人员来说是良好的编程习惯。数据结构的定义很大程度上决定了程序的可维护性和可扩展性。
接下来第三点是算法的说明、描述及测试子程序。
第四点,我们在强调BugFree,也就是调试能力以及编写无错代码的能力,一定要常常不忘做边界条件判断。
高级一点的开发人员,要注重第五点,程序框架设计的合理性。
第六点是程序的运行效率,这对于一个开发人员来说,已经到达一定高度了。更高的境界则是在开发程序的时候,还需要具备产品规划的能力,换句话说,良好的编程习惯能帮助你走向成功。
《程序员》:清您简单回顾一下您的程序员生涯。
雷军:我1987年进入武汉大学计算机系学习,在大一和大二期间,我一直在老师的实验室中做项目。大三、大四的时候在学校外面为一些公司开发相关的软件产品。1992年1月加入金山,写程序一直写到1997年。1997年以后我逐渐由一个程序员向一个管理者的角色过渡。
《程序员》:请给我们的读者几点具备实践意义的指导。
雷军:对于还在学校的计算机专业学生,首先他应当问问自己是否喜欢这个行业。因为写程序是一件非常辛苦的事情,如果没有发自内心的热情,是很难坚持下去的。
其次是多实践,多到老师的实验室去做项目。你编写的程序越多,对提高你自身能力的帮助越大。
对于那些已经进入到软件开发行业的人,则需要永不间断地学习新的技术。这是软件开发行业的特点,快速的发展,也需要你快速学习。
《程序员》:开发桌面软件与开发互联网软件有何区别?
雷军:开发互联网软件相对于开发桌面软件而言,技术复杂度有所增加。其实并不是技术的类别增加了,而是需要关注的点越来越多。另外,开发互联网软件的反应速度加快,一旦发现问题时,修正问题速度也相应加快,这时要对各种版本的软件加强管理,快速响应变化。
当然,写Web程序并非难事,但是要成为Web技术的高手,却并不容易。
《程序员》:您认为什么样的程序员才算是优秀的程序员或是程序高手?
王志东:对于程序员这个行业来说,十年前的情况和今天的现状是不能同日而语的。十年前,主流的开发语言很少,程序设计的方法也相对单一,开发人员很容易就能够掌握这些语言的基本功能和要点。现在随着程序语言不断发展和变化,主流程序语言出现了多元化格局,各种开发框架和架构层出不穷。
一个优秀的程序员,首先是应该具备非常强的学习能力。信息技术是在不断发展的,而且发展速度也越来越快。作为一个程序员只有通过不断的学习,才能掌握最新的技术,才能准确把握行业的发展趋势以及对自己所在的行业保持一种强烈的敏感度。
其次,信息时代查找和使用工具的能力。一名优秀的程序员平时可能会主动去收集和积累一些与之相关的资料,当需要这些信息时,可以随时查阅,然后将它用到实际的工作中去。
除此之外,合理利用外部资源的能力也很重要。因为互联网上有各种各样的技术社区,我们可以在这些社区中去寻求那些能够给予我们帮助的人。
当然,非常重要的一点是团队合作的能力。随着软件的产品化,程序员单兵作战的方式开始向团队合作的方式转变。任何好的软件产品,都必须在一个团队中以共同协作的方式来实现,需要大家的共同努力和配合。
《程序员》:哪些技能是一个程序员必备的基本技能?那些是最重要的?
王志东:其实,程序开发与具体行业应用是息息相关的。同样是程序员,他们在工作领域和工作模式上存在着极大差异。现在的程序员已经向着专业化的方向发展,不同的领域对每个程序员必备的技能的定义和要求不一样。比如说编程语言,有的行业要求C++的基础很扎实,有的行业则要求熟练掌握Java。对于一个大的软件工程项目来说,其涉及面极广,在不同的开发阶段对程序员要求很不一样,每个人都要发挥自己的特长。
《程序员》:能否简单回忆一下您的程序员生涯,并结合基本技能谈谈您自己在这个行业的经验?
王志东:我从大学开始编程,当时使用的编程语言以Basic和宏汇编为主,以C和C++为辅。真正的职业化程序员生涯,则是从1989年进入北大方正开始,到1994年结束。当时的编程以单兵作战为主,目标性也不强。直到在北大方正参与产品化工作。1994年后,我逐渐向职业经理人的角色转变,确切地说,1997年我才结束职业程序员生涯,完成了我的最后一行商业代码。当然,我在程序员中受益匪浅,有许多收获。
其一,养成了程序员的理性思维习惯。写程序和管理某些地方极为相似,比如合理利用有限资源达到预期目标。这个过程中养成的思维习惯给我带来了很大帮助。其二,让我始终能保持一种对技术的敏感。在互联网时代做软件创业,敏感的技术领域嗅觉将会给创业者带来很大的好处。其三,在从事软件产品化当中积累了团队经验。这为我后来创办新浪和点击科技带来了很大的帮助。另外,程序员的出身也让我非常重视技术人员,从而打造了良好的研发管理体系。
《程序员》:您如何通过一个简单的问题测试一个人是不是程序高手?
王志东:我会让他讲述值得回顾的工作或项目经历。当然,应试者的回答可以反映出许多重要信息。比如,他是否会热情洋溢地讲述自己的开发经验。这样可以判断面试者投入的精力与热情。我们需要热爱编程、热爱程序员职业的程序员。此外,我也非常希望点击科技的员工有更多编程以外的兴趣与爱好,让他们张扬个性,充分展现自己,让他们能够拥有更丰富的创造力。
《程序员》:请给我们的程序员读者几点实践指导。
王志东:我认为基本功非常重要。非常熟练地掌握一种编程语言,比如说C++或者Java是基本功之一。此外,数据库的基本原理以及数据结构的基本思想都是非常重要的基础理论。
另一方面就是动手实践。要想成为一名合格甚至优秀的程序员,仅仅只做书上的练习远远不够,必须亲自动手实践。只有在实际动手过程中你才会遇到书本上都没有的问题,学到书本上学不到的知识。
《程序员》:周总您好,请问在您眼里,什么样的人才是优秀的程序员?
周鸿袆:我认为优秀程序员应具备的素质主要有三点:第一,要有扎实的基本功。现在出现了很多新技术、新工具、新语言以及新平台,但是万变不离其宗,要深入理解数据结构、操作系统、体系结构这些基础知识。第二,要有比较开放的心态,不断学习,在坚实的基础之上融入新的东西。第三,有韧性,要坚忍不拔,还要耐得住寂寞。因为编程对于很多人来说都是件苦差事,如果过于浮躁,就很难有所成就。
《程序员》:您认为程序员必备的基本技能是什么?
周鸿袆:除了大学里所学的那些专业课,掌握操作系统级别的知识也很重要。可能现在的语言越来越先进,使得很多人写程序的时候不去关注太多细节,实际上如果你不了解每行代码如何起作用,你也就很难真正灵活运用代码去控制机器,所以说,对汇编语言和机器体系结构的了解也是非常重要的。
《程序员》:请您谈谈您写程序时期的几个重要历程。
周鸿袆:我接触计算机比较早,是从中学开始的。我当时用的是苹果机,用Basic语言编程,现在看来,这种教育是失败的,从结构化编程的角度来看,Basic语言注重玩弄一些并不重要的技巧,包括所谓的“一行程序”和“变量名越短越好”,那一代有些优秀的Basic程序员,后来反而无法适应新的技术发展。
大学里我的专业是计算机,我花了很多功夫在基础课的学习上,也打下了良好的基础。还做了很多实践,包括一些课题和软件,对我影响最大的语言是Borland公司的Turbo C和Turbo Pascal。研究生期间我开始接触VC,我很仔细地阅读了VC提供的源代码库。在研一的时候与同学合作了一个反病毒硬件,确切地说,是把软件固化在硬卡里,当时全部用的是汇编语言,这也使我进一步了解了IBM 386体系。我当时还做了一件事,就是把DOS系统反汇编,所以把DOS操作系统的源代码也通读了一遍。我的感触就是,如果把底层深刻的东西弄清楚,即使将来接触新的体系结构,包括新的操作系统、新的语言,你都会发现这些东西很容易掌握。
我研究生毕业后到了北大方正,在大型主机做Unix系统的开发,第一次接触这些内容,也没有感觉到特别大的障碍,我想这与之前打下的良好基础是分不开的。在北大方正,有个比较大的机会就是,第一次接触了互联网,后来开始做互联网软件。当时很多人应用互联网,都比较着迷表面上的东西,比如搭一个Web服务器,或者写一段网页,但是我把比较底层的互联网源代码、基础的协议都研究了一遍,对互联网有了更深入的理解,为后来做3721插件打下了很好的基础。
《程序员》:如果只允许您问一个问题,去判断一个程序员是不是高手,您会问什么问题?
周鸿袆:其实我比较反对这种所谓高手的说法。所谓术业有专攻,即便一个人在某个领域内有所专长,也很难冠以高手的称号。只问一个问题就判断一个人的真实水平,显然比较困难,但是如果只允许问一个问题的话,我想我有两个选择:如果考察基础,我会让他把C代码对应成相应的汇编语言;如果考察经验,我会让他讲一个曾经做过的项目,看看他是如何思考和解决问题的。
《程序员》:请您给想进入软件行业的学生,以及刚刚踏入这个行业的新人一些发展的建议。
周鸿袆:不要急于求成,先把基础打好。写程序就像练武功一样,内力越强,成就越大。所以初学者应该先把基础知识掌握好,以增强“内力”,越有“内力”,就越能适应各种变化,将来用哪种编程语言都只是“招数”而已。初学者要多花时间去阅读已有的源代码,很多前人留下来的代码都是值得借鉴和学习的例子;还要注重实践,如果没有编过10万行或者20万行代码,就很难成为好的程序员。
现在有种这样的说法:程序员吃的是青春饭,我认为这种言论是错误的。实际上,有些程序员在编程的道路上不能走得很远,主要是因为他们基础不好,不能适应行业的变化,与年龄无关。我认识一些优秀的程序员,40岁了还在写代码,他们的黄金期很长,原因就是技术基础比较好。
《程序员》:吴教授,您认为一名优秀的程序员需要具备什么知识,成为一名优秀的程序员要做哪些准备?
吴文虎:我认为基础很重要。编程是一个复杂的、脑力劳动与体力劳动结合的事情,它需要深厚的基础。这些基础不仅包括文化素养,也包括数理化文史地等各个方面。
徐明星:是的,其中包括文史地等社会科学的内容。从某种意义上说,程序设计也是一种艺术,需要艺术创造力。
吴文虎:软件开发是具有创见的、复杂的劳动。其中,数学基础非常重要。我在和一些中职和高职老师的交谈中,常常听老师谈道,学生一听数学就头大。我说那怎么行?那怎么可能学好程序设计?
《程序员》:现在的程序员不像冯·诺伊曼时代,只有数学家可以编程,您如何看待这件事?
吴文虎:数学实际上是一种思维方式。计算机先天与数学有关系。程序员是用计算机这个人类发明的智力工具来解决问题的,要解决问题,就必须善于把问题抽象化、形式化,建立数学模型。这就必须要求程序员懂数学。程序员也分层次,比如高级程序员、高级构架师,无论到哪个层次,都必须具备这种能力。所以,基础很重要,特别是数学基础。而且这个数学不是死记硬背、生搬硬套,而是怎么把数学和实际问题结合起来。就像徐明星讲的《信号处理原理》课,需要很多的数学知识,还需要较强的编程技术。
徐明星:我们要求学生不要把数学知识当作教条——这个公式解决这个问题,那个公式完成那个任务。而是在学习并掌握数学知识的过程中,培养自己的数学思维。比如我们在程序设计中,经常需要用到构造性的思维方法。这个方法,和数学中的加辅助线、引入辅助函数等思维方式是相似的。我们的程序设计也需要这样。所以,理解了这种构造性的数学方法,对于我们编写高质量的程序,是特别有意义的。一个高质量的程序,应该让人赏心悦目,读程序源码就应像看一篇好的小说,让人心情愉快。高质量的程序,让你可以清晰地看出作者思路。
《程序员》:因为计算机技术的快速发展,很多人说高校的计算机课程设置与实际应用脱节,您二位怎样看待?
吴文虎:是有这样的现象,我们的课程(指给大一学生开设的《程序设计基础》课)也在不断调整。但是在学校里,重要的是打基础。我们通过具体的课程传授科学的世界观和方法论,也就是我们要授人以渔,而不是授人以鱼。
徐明星:我个人觉得,学习计算机技术并从事相关领域的工作,是最有趣味的——因为你总有新东西要去学习,需要活到老学到老。因为计算机技术发展非常迅速,你总是能够接触到新知识、技术和应用,总会发现新的灵感。
吴文虎:另外,这个基础不是建筑学意义上的基础,而是能够不断成长的“生物学”意义上的基础。编程中的数学能力要落实到解决实际问题上,纸上谈兵是不行的。编程的创造力表现在观察能力,思维能力和实践能力上。
徐明星:除了智商外,编程也需要情商。现代软件、商业软件都不是一个人单枪匹马能够完成的,通常需要多人合作,所以协作能力非常重要。
《程序员》:一个没有机会上大学的人想从事软件开发,补修那些课程比较重要?
徐明星:他可以直接通过实践来解决发现的问题,因为这种人很多都没有大量时间。
吴文虎:这是我们课程教学小组一直关注的问题,我们叫它“任务驱动法”,通过一些生动的问题,帮助学生理解计算机技术的核心思想。
徐明星:任务驱动方法有些像近几年流行的英语学习“逆向法”。每解决一个问题,水平就提高一点,水平的提高也可以切实感受到!这一点对自学者非常重要。所以任务驱动法通过解决实际问题来学习专业知识,这样更有兴趣,也更能持久,让程序员获得更多成就感。
《程序员》:刘老师您好,请问在您眼里,什么样的人才是优秀的程序员?
刘建国:我认为,判断一个程序员是否优秀可以从两个方面来考察:软件能力和硬件能力。当然,我这里的“软件”和“硬件”并不是指计算机软硬件,而是指人的素质。硬件方面的素质包含两个方面:首先,扎实的基础,比如对于计算机科学的基本知识,包括操作系统和体系结构的掌握;其次,较强的动手能力,善于把想法付诸实践,并且保证作出高质量的软件。软件素质同样包含两个方面:第一,善于钻研和学习,喜欢技术,只有自己真正感兴趣,才会融入其中。能够自我激励和管理,自己给自己设定目标,而不是等待别人去安排任务。第二,要有较强的团队协作能力,现在的时代已经不是单打独斗的时代,所以一个优秀的程序员,必须善于与人合作,在团队中起到促进的作用。
《程序员》:您认为程序员必备的基本技能是什么?
刘建国:我认为,素质是最重要的,没有必要具体到某种技能上面。编程语言只要精通一种,就可以触类旁通,不一定非要掌握某种语言。你可以精通面向对象语言如C++,也可以选择Java,只要基础扎实,善于学习,就可以取得一定的成就。
《程序员》:请您谈谈您写程序时期的几个重要历程。
刘建国:我的编程历程其实很平凡,加入百度之前,都是自始至终按部就班地走过来的。在大学时期,主要是打基础,我上大学的时候条件不好,动手机会不多,尤其是大学的前两年。后来从大三开始有一些上机实践的机会,内容主要是完成一些课程作业。直到现在我还认为,课程作业是很重要的实践机会,不容忽视。大学毕业后,考上了北大的研究生,实践机会更多了。我的研究方向是互联网,研究广义的网络,包括网络协议的分析,网络服务器的实现,以及网络应用方面的内容。之所以选择这个方向,是因为我认为,这些内容是底层的东西,相对比较基础,只有真正搞技术的人才能做,而且当时网络的发展处于方兴未艾的阶段,很有前景。这个阶段,我在编程的时候,比较注重总结,我会把自己编程的一些优点和不足都记录下来,从中提炼有价值的内容,这使我的编程技能不断进步。
毕业之后留校,成为老师,经常有机会带学生做项目。无形中就做了很多管理的工作,包括项目管理,还有整个系统的设计,这为以后在百度做管理的工作也打下了基础。后来接触互联网比较多了,感觉搜索引擎是个很好的工具,于是就慢慢转向了搜索引擎领域的研究。然后就离开了北大,加入百度,带领开发团队去写代码,搭建百度的搜索引擎平台,从技术到管理,形成了一个很自然的过渡。
《程序员》:如果只允许您问一个问题,去判断一个程序员是不是高手,您会问什么问题?
刘建国:我觉得一个问题是远远不够的,即便是一组问题,也很难问出来。我面试员工的时候,主要考察两个方面,就是我刚刚提过的硬技能和软技能,在硬技能相当的情况下,我更偏重软技能的考察。硬技能决定起步,但软技能决定发展,如果一个人有较强的学习能力,善于总结,敢于承担责任,他就会成长得很快。
《程序员》:请您给想进入软件行业的学生,以及刚刚踏入这个行业的新人一些发展的建议。
刘建国:我认为最重要的就是注重基础知识的学习,形成扎实的基本功,并且在工作中不断培养自己的软技能。每个阶段都有最重要的事情,要尽力把手中的每件事都做到最好,时刻激励自己,不要急功近利,其实有时慢就是快,快反而是慢。如果现在一步步稳扎稳打,以后可以做得很快,反过来如果现在做得过急,那以后反而会进步得很慢。如果具体到搜索引擎领域,我认为进入这个领域没有特别的门槛,只不过涉及的内容比较广泛,包括分布式数据、人机交互等比较难的问题,但是只要具备足够的软技能,无论你起点如何,你都能够做好。
《程序员》:我看过你那本书《编程高手箴言》,很有启发,那本书中提到的内容现在依然有效吗?您认为什么样的人是程序高手?
梁肇新:不行,那本书太旧了。说起来比较惭愧,已经很长时间没有更新那本书的内容了。尤其是现在写程序的时间也没有原来那么长,将更多的时间花在了管理上。
我认为真正的程序高手是能够解决一切问题的人。也就是说,任何技术难题在他们那里都能得到解决,当然,这种人并不多见。
《程序员》:请您谈谈自己在从事软件开发中的几个阶段。
梁肇新:从个人编程技能的提升角度看,写程序主要分两个阶段:一个是在学校的阶段(梁先生这个时候,做了一个手势,指了指三寸厚的桌面),学校的阶段相当于在桌面以下,另一个阶段则是桌面以上,从桌面一直到想有多高,就有多高(这时候,梁先生看了一眼天花板)。
一般人很难在学校的那个阶段跨越断层,这是需要用实践来积累的。写程序没有太多窍门,必须要投入大量的精力到实践当中,才能成为真正的编程高手。一般很少有在校学生能够突破这个断层,有的那个部分一般都在学校上学阶段做了不少项目。而通过读书的方式,只能加强理论方面的认识,对于程序员开发能力的提高起到的作用很小。
《程序员》:从第一个阶段,到第二个阶段,您花了多长时间?
梁肇新:两个月。那时候在学校花费了很大的精力来阅读源代码,当时Unix下的一部分的源代码被我全部用手抄写下来,记了足足三大本(梁先生一边说一边比划出三个笔记本的厚度)。当然,那个时代和现在已经不一样了。
刚才说到从个人编程机能的角度。另外从时代发展的角度来看,也分为了不同的阶段。早期DOS流行的时候是硬件时代,那个时候的计算机系统非常简单,当然只是相对现在简单,琢磨透了也就能够成为编程高手了,当然,也需要懂一些汇编语言的基础。随后,Windows带来了软件时代。这个时候的程序员才开始大量生产程序,那个时代的程序员一般都需要掌握Windows的平台机制,同时掌握一种如C/C++之类的编程语言,了解面向对象的方法。这里指的是方法,而不是思想,解决问题总是靠方法,而不是思想的,我更偏向于实践。再接下来就是当今的网络时代,程序员必须懂得页面编程,就好比我们刚刚发布的产品,也是需要基于Web来完成的。
《程序员》:您认为一个程序高手最重要的技能是什么?
梁肇新:调试。我相信无论什么程序都是调试出来的。我们这儿有个程序员,一开始是系统管理员,后来因为碰到一个问题,他花了很多时间来实践,最后解决了,我认为这样的人可以做开发。当然,在解决实践问题的过程当中,也需要讲究方法,这也是我在前面提到面向对象方法的原因。在学校里,我们都很喜欢各种理论,各种新概念。但是出来做事,能真正解决问题的才有意义。这也是我认为调试在程序设计中最为重要的原因。
《程序员》:贵公司招聘时,您怎样甄选人才?
梁肇新:这种时候总是要因人而异的。我们需要的是适合某个职位的人,如果招聘员工做客户端程序开发,就要C/C++比较好的人员;若是做网络平台开发,则要了解.NET技术,因为我们公司的产品是基于.NET的。
《程序员》:如果让您通过一个问题来鉴别一个人是不是程序高手,您会问什么?
梁肇新:病毒!是不是做过病毒?(笑)换句话说,是否自己写过杀毒软件。能做病毒的人,一般都是对计算机系统比较了解的人。就像前段时间影响网络的“熊猫烧香”病毒,那个作者应该还不错。当然,那个程序本身写得并不太好,从开发的方式可以看出来,他是用Delphi写的,有些重画写的不太好。
《程序员》:请谈谈您所定位的优秀程序员应该有哪些技能?您是怎样理解程序员七种武器的?
周爱民:一般来说,剑客用剑、刀客用刀,所以所谓七种武器,用在手上的,或许也就一件两件。但如果以技巧、技法论,却不是一件两件兵器就可以胜出江湖的。例如不懂刀的剑客,遇到用刀的人时便不知道如何刺击了。因为要先知其长短,才能避强而击弱。
不懂刀的剑客与不懂剑的刀客对打起来,大概会如同演舞一般,各练各的招式。
所以,优秀的程序员或程序高手并不是通晓七种武器的全才,必然是精通其一,且概知其它。与江湖不同的是,我们事实上并不拿一种武器去与拿另一种武器的人比斗。我们的对象是一个软件,或者某个工程。我们要克“敌”制胜,而敌是死物,不是活人。换言之,我们了解这些技术方法的目的,并不是要站到擂台上去比较谁懂得多,或者谁精通什么。我们只是要做一个软件或者具体的工程,那么所谓技术方法,只要对这个软件或工程有效即可。因此,用汇编写内核的人不必看不上用C写协议层的人,用C++写平台应用的,也不必看不上用SQL写脚本的人。放在一个团队里,汇编、C、C++和SQL在一个项目里可能各有其用,非得让这些人分个高下出来,最终是项目的失败。
任何的一种工具都有使用它的境界。很多人看不到这一点,而轻视其它语言或者工具。例如SQL,几乎所有使用高级语言(我当然不是说它比别的语言好而显得“高级”,而只是存在着这么一个分类)的开发人员都认为SQL是“一种相当简单的脚本”。但事实上,我的朋友中就有能把SQL用到出神入化的,他能由SQL的写法来推断数据库设计的失败,或者反过来,为特定的数据库系统写出最优化的SQL。重要的是,这个朋友会针对不同的存取环境、网络结构而设计特定的数据库和SQL,以达到最优。
优秀的程序员是对工具没有偏见的、能适应场合活用语言的人;而程序高手则基于这个前提,并专精其一,进而通一晓十的人。
《程序员》:您认为什么是程序员的基本技能?哪些是最重要的?
周爱民:“七种武器”中,数据结构是成为真正的程序员的基础,而面向对象思想则是门径。
程序员能否在软件开发这条道路上走下去,很大程度上取决于他对数据结构的了解。任何一种新的语言,或者任何一种新的体系结构的出现,都可以在数据结构上找到相关的解释。
因此,程序员如果能精通数据结构,那么相当于拥有了“以不变应万变”的资本。对此,《人月神话》中用一句话指出了关键:“数据的表现形式是编程的根本”。
对象是数据结构抽象的一种,但并不是唯一的一种。从这个角度上来说,对象并不是唯一的编程之道。我曾经说,人造卫星也是在面向过程的时代上的天。也就是说,面向过程也可以组织足够复杂的程序。
然而,所有这些都并不能否认面向对象的价值。面向对象是对事物的本体特性与行为特性的高度抽象,它将数据结构从“死的内存”变成了“活的物件”。面向对象的思想使我们在增强对现实的表达能力的同时,避免了复杂数据结构带来的藕合。由面向对象、信息隐藏和接口抽象三个相关联的概念,构成了整个软件体系设计的理论基础。这三个概念中,面向对象是与软件开发人员关系最密切,最容易理解的,因此它是使你成为真正的程序员,以及从程序员走向程序设计师的最佳门径。
《程序员》:如果用一个问题来测试一个人是不是程序高手,您会怎样问?
周爱民:我的问题是:你参与或组织过怎样的开源项目,如何评价它?
在程序高手这个级别上,能脱颖而出的是那种有合作能力的、思想开放的优秀人才,而绝不是技术高下的简单辨别。而观察他对开源项目的经验和兴趣,是一种不错的方式。
这里说的不是“把源代码公开”就是开源项目了。我说的是真正有组织的、持续的、公开源码的项目运作。之所以做这么多的限定条件,是因为现在很多人都已经接受了“源码公开”的思想,但这仅表明这个人有了分享的精神,并不表明他有组织和管理项目的能力。开源项目的生命力是在项目管理者在长时间的维护过程中得以延续的,同样的,项目管理者也在这个过程中历练了自己的技能与心性。而这些,正是高手在成长中不可或缺的要素。
《程序员》:您在成为一个优秀的开发人员过程中,哪几个阶段是最让您难忘的?
周爱民:我做程序之初,只是喜欢而已,其实真的是楞头青。这个阶段看来,就是代码不规范、接口随意,并且经常会推翻重来。“代码不规范”是非专业人员的通病,我在很长一段时间里,都有“自己的风格”,所以还专为这个跟以前的部门经理争辩过:我同意格式化呀,但为什么要按你说的格式呢?“接口随意”是没有设计就开始写代码的表现,而“经常推翻重来”则是其后果。非专业人员最初通常就是一个人开始练手,自己给自己写代码,接口怎么写自己都能理解,所以专门去设计反倒是麻烦。但到了团队里面,过于“独特的”程序接口则是灾难,因为你得去给每个人解释这个接口的用法,说服他们使用这种接口。而这往往会遵行强势原则:你要么屈从“更标准的接口设计”,要么团队就放弃你的这些代码。
做程序再熟练,过不了上面这个阶段就谈不上合作,也谈不上设计。一个人写程序,无需多少设计的功夫。但一个团队合作,没有设计就不行。因此我认为写程序的第二个阶段就是团队开发和专业设计。而这两点,正是从项目管理和技术实现上来组织大规模开发的不二法门。因此,我事实上在这里想说明的是:相对于个体开发,团队开发是更高阶的技术。
在你学会了团队开发,能够轻松地与人合作,或组织小型的开发团队时,要想在这个行业中安身立命就并非难事了。至于用哪种语言,由于你是“优秀的程序员”,因此语言的选择是应项目之所需的,所以不是接下来要谈的关键问题。
第三个阶段是你能否在行业中脱颖而出的关键。但这个关键与技术无关,而是一个人的秉性和个性的问题。我们一方面会很阿Q地说“酒香不怕巷子深”,另一方面又说“千里马常有而伯乐不常有”。问题在于,马不能主动地找伯乐,人又为什么不能呢?马困于厩而显凡俗,人立于世可显不群。有表现自己不凡的品质的空间而不施展,根本上说还是能力问题。所以学会沟通、交往,而不是沉迷于代码,可能是第三个阶段的重要瓶颈。
在第三个阶段,你可能面临非常多的选择。例如技术主管、项目经理或者设计师、分析师之类。但你应该会发现,这所有的选择都将使你被推到团队的前面,你必须面对整个团队,以及项目的干系人(例如客户)。而能否胜任这些,取决于你的综合素质,而非单一的软件开发技能。
最后你得记住一件事,上面的这个过程,不是一朝一夕,也不是一年两年,而是五年十年的时间。在这个过程中,所有成功者都必须具备的,是认真的态度和专业的精神。
《程序员》:请为开发人员提供几点实践性的指导。
周爱民:把语言比作“称手的兵器”,那么基本技能则相当于内家修为。练石锁也能练出个李元霸,这说明单单靠“不停地写程序”,也是能写出高手来的。但是,如果一上手就给个千百斤的大石锁,李元霸没练成便先牺牲了。所以凡事都有个循序渐进,所循的这个“序”,并不要求每个人都相同,别人的经验,大抵上适合做个参考。而我也不能言讲什么指导性的东西,所以上面所谈,大家尽可以当经验来看,当参考来用。是实践,却不是什么指导。
《程序员》:请您谈谈做游戏开发与一般的软件开发,在技能上的要求有什么不同?
周爱民:游戏开发涉及的领域是比较复杂的。例如对界面交互,一般软件开发中有可用性测试,而游戏中叫可玩性测试。可用与可玩,就已经是两种不同的界面交互设计理念了。一般软件开发很少在界面部分应用人体工程学的知识,而游戏界面交互设计中却经常要用到这种知识。但是同样的例子,如果你做游戏开发中的网络传输或者服务器端,就涉及不到人体工程学。所以这里要说的是,现在游戏开发过程被分解得很细,不同的技能在游戏开发领域中都可以找到位置,但不要指望能什么技能都精通,然后一两个人就搞完整套游戏。
大多数游戏开发能涉及到的领域,在一般的软件开发中也同样会涉及到。例如数据库,很多人认为数据库与做游戏风马牛不相及。但事实上,在游戏开发中,后台数据库的存取效能、分布特性等是严重影响游戏体验的。所以你在传统软件开发中做得很好的技能,在游戏开发中一样用得到。
然而不同之处还是有的,其中突出的几点表现在视觉特效、交互特性和网络性能。
一般性的软件开发中,我们会遵从操作系统的惯例为用户提供交互体验,但游戏正好是希望给用户独特的体验,因此通常有不同的交互特性。这可能小到一个按钮的设计,大到整个操作的流程。这种交互特性又与输入输出设备的性质相关,例如手机的屏幕与键盘与PC就不一样。所以游戏对整个系统输入输出的研究,与操作系统和一般软件是不一样的,根源在于它要提供独特性。
游戏对网络层的研究,也与传统软件不一样,但这不是独特性导致的,而是用户量级的问题。大到银行、电信这样的系统中,人们对数据传输的效能通常是由数据库系统和硬件系统来保证的,因此你只需要研究数据和库的优化。但我们总不能让用户花钱买完MS SQL、架完专线再来玩游戏,所以游戏开发中要在相当高的数量级上,自己来解决数据传输和数据库使用中的问题。然而游戏是多用户、强交互的系统,因此很快爆发出来的问题是分布问题、并行问题等等。这些原本在其它开发中交给某个专属领域去解决的问题在游戏中都需要用自己的方法去解决。
理解正则表达式(上)
在程序员日常工作中,数据处理占据了相当的比重。而在所有的数据之中,文本又占据了相当的比重。文本能够被人理解,具有良好的透明性,利于系统的开发、测试和维护。然而,易于被人理解的文本数据,机器处理起来就不一定都那么容易。文本数据复杂多变,特定性强,甚至是千奇百怪。因此,文本处理程序可谓生存环境恶劣。一般来说,文本处理程序都是特定于应用的,一个项目有一个项目的要求,彼此之间很难抽出共同点,代码很难复用,往往是“一次编码,一次运行,到处补丁”。其程序结构散乱丑陋,谈不上有什么“艺术性”,基本上与“模式”、“架构”什么的无缘。在这里,从容雅致、温文尔雅派不上用场,要想生存就必须以暴制暴。
事实上,几十年的实践证明,除了正则表达式和更高级的parser技术,在这样一场街头斗殴中别无利器。而其中,尤以正则表达式最为常用。所以,对于今天的程序员来说,熟练使用正则表达式着实应该是一种必不可少的基本功。然而现实情况却是,知道的人很多,善于应用的人却很少,而能够洞悉其原理,理智而高效地应用它的人则少之又少。大多数开发者被它的外表吓倒,不敢也不耐烦深入了解其原理。事实上,正则表达式背后的原理并不复杂,只要耐心学习,积极实践,理解正则表达式并不困难。下面列举的一些条款,来自我本人学习和实践经验的不完全总结。由于水平和篇幅所限,只能浮光掠影,不足和谬误之处,希望得到有识之士的指教。
了解正则表达式的历史
正则表达式萌芽于1940年代的神经生理学研究,由著名数学家Stephen Kleene第一个正式描述。具体地说,Kleene归纳了前述的神经生理学研究,在一篇题为《正则集代数》的论文中定义了“正则集”,并在其上定义了一个代数系统,并且引入了一种记号系统来描述正则集,这种记号系统被他称为“正则表达式”。在理论数学的圈子里被研究了几十年之后,1968年,后来发明了UNIX系统的Ken Thompson第一个把正则表达式用于计算机领域,开发了qed和grep两个实用文本处理工具,取得了巨大成功。在此后十几年里,一大批一流计算机科学家和黑客对正则表达式进行了密集的研究和实践。在1980年代早期,UNIX运动的两个中心贝尔实验室和加州大学伯克利分校分别围绕grep工具对正则表达式引擎进行了研究和实现。与之同时,编译器“龙书”的作者Alfred Aho开发了Egrep工具,大大扩展和增强了正则表达式的功能。此后,他又与《C程序设计语言》的作者Brian Kernighan等三人一起发明了流行的awk文本编辑语言。到了1986年,正则表达式迎来了一次飞跃。先是C语言顶级黑客Henry Spencer以源代码形式发布了一个用C语言写成的正则表达式程序库(当时还不叫open source),从而把正则表达式的奥妙带入寻常百姓家,然后是技术怪杰Larry Wall横空出世,发布了Perl语言的第一个版本。自那以后,Perl一直是正则表达式的旗手,可以说,今天正则表达式的标准和地位是由Perl塑造的。Perl 5.x发布以后,正则表达式进入了稳定成熟期,其强大能力已经征服了几乎所有主流语言平台,成为每个专业开发者都必须掌握的基本工具。
掌握一门正则表达式语言
使用正则表达式有两种方法,一种是通过程序库,另一种是通过内置了正则表达式引擎的语言本身。前者的代表是Java、.NET、C/C++、Python,后者的代表则是Perl、Ruby、JavaScript和一些新兴语言,如Groovy等。如果学习正则表达式的目标仅仅是应付日常应用,则通过程序库使用就可以。但只有掌握一门正则表达式语言,才能够将正则表达式变成编程的直觉本能,达到较高的水准。不但如此,正则表达式语言也能够在实践中提供更高的开发和执行效率。因此,有心者应当掌握一门正则表达式语言。
理解DFA和NFA
正则表达式引擎分成两类,一类称为DFA(确定性有穷自动机),另一类称为NFA(非确定性有穷自动机)。两类引擎要顺利工作,都必须有一个正则式和一个文本串,一个捏在手里,一个吃下去。DFA捏着文本串去比较正则式,看到一个子正则式,就把可能的匹配串全标注出来,然后再看正则式的下一个部分,根据新的匹配结果更新标注。而NFA是捏着正则式去比文本,吃掉一个字符,就把它跟正则式比较,匹配就记下来:“某年某月某日在某处匹配上了!”,然后接着往下干。一旦不匹配,就把刚吃的这个字符吐出来,一个个的吐,直到回到上一次匹配的地方。
DFA与NFA的机制不同带来5个影响:
1. DFA对于文本串里的每一个字符只需扫描一次,比较快,但特性较少;NFA要翻来覆去吃字符、吐字符,速度慢,但是特性丰富,所以反而应用广泛,当今主要的正则表达式引擎,如Perl、Ruby、Python的re模块、Java和.NET的regex库,都是NFA的。
2. 只有NFA才支持lazy和backre-ference等特性;
3. NFA急于邀功请赏,所以最左子正则式优先匹配成功,因此偶尔会错过最佳匹配结果;DFA则是“最长的左子正则式优先匹配成功”。
4. NFA缺省采用greedy量词(见item 4);
5. NFA可能会陷入递归调用的陷阱而表现得性能极差。
我这里举个例子来说明第3个影响。
例如用正则式/perl|perlman/来匹配文本 ‘perlman book’。如果是NFA,则以正则式为导向,手里捏着正则式,眼睛看着文本,一个字符一个字符地吃,吃完 ‘perl’ 以后,跟第一个子正则式/perl/已经匹配上了,于是记录在案,往下再看,吃进一个‘m’,这下糟了,跟子式/perl/不匹配了,于是把m吐出来,向上汇报说成功匹配‘perl’,不再关心其他,也不尝试后面那个子正则式/perlman/,自然也就看不到那个更好的答案了。
如果是DFA,它是以文本为导向,手里捏着文本,眼睛看着正则式,一口一口地吃。吃到/p/,就在手里的‘p’上打一个钩,记上一笔,说这个字符已经匹配上了,然后往下吃。当看到 /perl/ 之后,DFA不会停,会尝试再吃一口。这时候,第一个子正则式已经山穷水尽了,没得吃了,于是就甩掉它,去吃第二个子正则式的/m/。这一吃好了,因为又匹配上了,于是接着往下吃。直到把正则式吃完,心满意足往上报告说成功匹配了‘perlman’。
由此可知,要让NFA正确工作,应该使用 /perlman|perl/ 模式。
通过以上例子,可以理解为什么NFA是最左子式匹配,而DFA是最长左子式匹配。实际上,如果仔细分析,关于NFA和DFA的不同之处,都可以找出道理。而明白这些道理,对于有效应用正则表达式是非常有意义的。
理解greedy和lazy量词
由于日常遇到的正则表达式引擎全都是NFA,所以缺省都采用greedy量词。Greedy量词的意思不难理解,就是对于/.*/、/\w+/这样的“重复n”次的模式,以贪婪方式进行,尽可能匹配更多字符,直到不得以罢手为止。
举一个例子,以/<.*>/模式匹配 ‘<book> <title> Perl Hacks </title></book>\t’文本,匹配结果不是‘<book>’,而是‘<book> <title> Perl Hacks </title> </book>’。原因就在于NFA引擎以贪婪方式执行“重复n次”的命令。让我们来仔细分析一下这个过程。
条款3指出,NFA的模型是以正则式为导向,拿着正则式吃文本。在上面的例子里,当它拿着/.*/这个正则式去吃文本的时候,缺省情况下它就这么一路吃下去,即使碰到‘>’字符也不罢手——既然 /./ 是匹配任意字符,‘>’当然也可以匹配!所以就尽管吃下去,直到吃完遇到结尾(包括\t字符)也不觉得有什么不对。这个时候它突然发现,在正则表达式最后还有一个 />/,于是慌了神,知道吃多了,于是就开始一个字符一个字符地往回吐,直到吐出倒数第二个字符‘>’,完成了与正则式的匹配,才长舒一口气,向上汇报,匹配字符串从第一个字符‘<’开始,到倒数第二个字符‘>’结束,即‘<book> <title> Perl Hacks </title> </book>’。
Greedy量词的行为有时确实是用户所需要的,有时则不是。比如在这个例子里,用户可能实际上想得到的是‘book’ 串。怎么办呢?这时候lazy量词就派上用场了。把模式改为/<.*?>/就可以得到 ‘book’。这个加在‘*’号后面的‘?’ 把greedy量词行为变成lazy量词行为,从而由尽量多吃变为尽量少吃,只要吃到一个‘>’立刻停止。
问号在正则表达式里用途最广泛,这里是很重要的一个用途。
理解backtracking
在条款4的基础上解释backtracking就很容易了。当NFA发现自己吃多了,一个一个往回吐,边吐边找匹配,这个过程叫做backtracking。由于存在这个过程,在NFA匹配过程中,特别是在编写不合理的正则式匹配过程中,文本被反复扫描,效率损失是不小的。明白这个道理,对于写出高效的正则表达式很有帮助。
在下期文章当中,将会向您介绍如下几个部分的内容:
· 理解grouping、capturing和lookaround
· 了解正则表达式的编译执行过程
· 理解modifier的意义和作用
· 合理的“拿来主义”
· 该收手时就收手等其它内容
调试之剑
弗雷德里克·布鲁克斯(Frederick P. Brooks)博士在他那篇著名的《没有银弹——软件工程中的根本和次要问题》一文中,将软件项目比作可怕的人狼(werewolves),并大胆地预言十年内不会找到特别有效的银弹。该论文发表的时间是1986年,如今整整20年过去了,尽管不时有人惊呼找到了神奇的银弹,但是冷静的人们很快发现那只是美好的愿望。
如果说软件工业中与人狼的战斗还在持续,那么在这些战役中一定会有程序员的身影,笔者也是其中的一个。我的编程生涯是从使用汇编语言编写DOS下的TSR程序开始的。今天DOS操作系统已经成为历史,在那个年代最值得炫耀的TSR技术也早已经过时了。十几年中,OWL、VFW、VDX、ISAPI、Active Movie等技术也被时间淘汰……然而,在这漫长的时间当中,我最看重的是软件调试技术。它是十几年中我学到的最有用、一直受用、而且日久弥新的一项技术。
从软件工程的角度来讲,软件调试是软件工程的一个重要部分,软件调试过程出现在软件工程的各个阶段。从最初的可行性分析、原型验证、到开发和测试阶段、再到发布后的维护与支持,都有软件调试过程的参与。通常认为,一个完整的软件调试过程由以下几个步骤组成:
· 重现故障,通常是在用于调试的系统上重复导致故障的步骤,使要解决的问题出现在被调试的系统中。
· 定位根源,即综合利用各种调试工具,使用各种调试手段寻找导致软件故障的根源(root cause)。通常测试人员报告和描述的是软件界面或工作行为中所表现出的异常,或者是与软件需求和功能规约不符的地方,泛指软件缺欠(defect)或者故障(failure)。而这些表面的缺欠总是由于一或多个内在因素所导致的。这些内因要么是代码的行为错误,要么是不行为错误(该作而未作)。
· 探索和实现解决方案,即根据寻找到的故障根源、和资源情况、紧迫程度等要求设计和实现解决方案。
· 验证方案,在目标环境中测试方案的有效性,又称为回归(regress)测试。如果问题已经解决,那么就可以关闭问题。如果没有解决则回到第3步调整和修改解决方案。
这些步骤中,定位根源常常是最困难也是最关键的步骤,它是软件调试过程的核心和灵魂。如果没有找到故障根源,那么解决方案便很是隔靴搔痒,或者头痛医脚,白白浪费了时间。
对软件调试的另一种更通俗的解释是指使用调试工具求解各种软件问题的过程,例如跟踪软件的执行过程,探索软件本身或者与其配套的其它软件或者硬件系统的工作原理等,这些过程的目的有可能是为了去除软件缺欠,也可能不是。
在了解了软件调试技术的基本概念以后,下面我们来看一下支撑软件调试技术的几种基本机制。
· 断点:即当被调试程序执行到某一空间或时间点时将其中断到调试器中。根据中断条件分为如下几种:
○ 代码断点:当程序执行到指定内存地址的代码时中断到调试器。
○ 数据断点:当程序访问指定内存地址的数据时中断到调试器。
○ I/O断点:当程序访问指定I/O地址的端口时中断到调试器。
根据断点的设置方法,断点又分为软件断点和硬件断点。软件断点通常是通过向指定的代码位置插入专用的断点指令来实现的,比如IA32 CPU的INT 3指令(机器码为0xCC)就是断点指令。硬件断点通常是通过设置CPU的调试寄存器来设置的。IA32 CPU定义了8个调试寄存器,DR0~DR7,可以最多同时设置4个硬件断点(对于一个调试会话)。通过调试寄存器可以设置以上三种断点中的任一种,但是通过断点指令只可以设置代码断点。
· 单步跟踪:即让应用程序按照某单位一步步执行。根据单位,又分几种:
○ 每次执行一条汇编指令,称为汇编语言一级的单步跟踪。设置IA32 CPU标志寄存器的TF(Trap Flag,即陷阱标志位)位,便可以让CPU每执行完一条指令便产生一个调试异常(INT 1),中断到调试器。
○ 每次执行源代码(比汇编语言更高级的程序语言,如C/C++)的一条语句,又称为源代码级的单步跟踪。通常高级语言的单步跟踪是通过反复设置CPU的陷阱标志位来实现的,如果当前源代码行还没有执行完,那么调试器重新设置陷阱标志并让程序继续执行,直到该语句结束(EIP指向另一语句)才中断给用户。
○ 每次执行一个程序分支,又称为分支到分支单步跟踪。设置IA32 CPU的DbgCtl MSR寄存器的BTF(Branch Trap Flag)标志后,便可以启用分支到分支单步跟踪。
○ 每次执行一个任务(线程),即当一个任务(线程)被调度执行时中断到调试器。IA32架构所定义的任务状态段(TSS)中的T标志为实现这一功能提供了硬件一级的支持,但是很多调试器还有提供这项功能。
· 栈回溯(stack backtrace):即通过记录在栈中的函数返回地址显示(追溯)函数调用过程。在将返回地址翻译成函数名时需要有调试符号(debug symbol)的支持。大多数编译器都支持在编译时生成调试符号。微软的调试符号服务器(http://msdl.microsoft.com/download/symbols)提供了大多数Windows系统文件的调试符号,是调试和学习Windows操作系统的宝贵资源。
· 调试信息输出(debug output/print):即将程序运行的位置、变量状态等信息输出到调试器、窗口、文件或者其它可以观察到的地方。这种方法的优点是简单方便、不依赖于调试器,但也有明显的缺点,如效率低,安全性差,通常不可以动态开启,且难以管理等。在Windows操作系统中,驱动程序可以使用DbgPrint/DbgPrintEx来输出调试信息,应用程序可以调用OutputDebugString API。
· 日志(log):将程序运行的状态信息写入到特定的文件或者数据库中。Windows操作系统提供了记录、观察和管理(删除和备份)日志的功能。Windows Vista新引入了名为Common Log File System (CLFS.SYS)的内核模块,用于进一步加强日志功能。
· 事件追踪(event trace):通常用来监视频繁的复杂的软件过程,满足普通日志机制难以胜任的需求。比如监视大信息量的文件操作、网络通信等。ETW(Event Trace for Windows)是Windows操作系统内建的事件追踪机制,Windows内核本身和很多Windows下的软件工具(如Bootvis,TCP/IP View)都使用了该机制。
在以上机制中,断点和单步跟踪通常必须在有调试器参与的情况下才能使用。调试器(software debugger)是综合提供各种调试功能的软件工具。除了处理断点、单步跟踪、模块映射等调试事件外,调试器通常还提供如下功能:
· 观察和编辑被调试程序的内存和数据,如全局变量、局部变量、以及程序的栈和堆等重要数据结构。
· 观察和反汇编被调试程序的代码。
· 显示线程栈中的函数调用信息。
· 管理调试符号。
· 控制进程和线程,例如将被调试程序中断到调试器中,和恢复其执行等。
根据调试器所调试目标程序的工作模式,可以把调试器分为用户态调试器和内核态调试器,前者用于调试用户态下的各种程序(应用程序、系统服务、或者用户态的DLL模块),后者用于调试工作在内核模式的程序,如驱动程序和操作系统的内核部分。WinDbg是微软开发的一个免费调试器,它既可以用作用户态调试器,也可以用作内核态调试器,是调试Windows操作系统下的各种软件的一个强有力工具。我几乎每天都使用WinDbg,它是我的计算机中使用频率最高的软件之一。
最后,简要地描述一下软件调试技术的几个特征。
系统性——很多看似简单的调试机制都是依靠系统内的多个部件协同工作而完成的。以软件断点为例,CPU提供了指令支持和硬件级的异常机制,操作系统将异常以调试事件的形式分发给调试器,调试器响应调试事件并与用户交互。如果在做源代码级的调试,那么调试器又需要编译器所产生的调试符号来帮忙。
全局性——对于一个软件项目,应该在项目的设计和架构阶段就制定出全局的调试支持机制,并贯彻实施。比如,所有模块都应该使用统一的方法来输出调试信息、记录日志、报告错误,并公开统一的接口用做单元测试和故障诊断。这样不仅可以避免重复工作,而且增加了软件的可调适性(debuggability),有利于保证产品的质量和进度。
困难性——《C语言编程》一书的作者Brian Kernighan曾经说过,“调试天生就比编写代码难上一倍,如果你写出了最聪明的代码,那么你的智商就不足以调试这个代码。”因为,要调试一个程序,就必须深刻理解它的工作原理,不仅要知道how和表层的东西,还要知道why和深层次的内幕。另外,调试需要锲而不舍的探索精神和坚韧的耐力,这也让很多人望而却步。
综上所述,软件调试技术是与软件开发密不可分的一门技术,其初衷是为了定位和去除软件故障,但因为调试技术所具有的对软件的强大控制力和观察力,其应用早已延伸到了很多其它领域,比如逆向工程、计算机安全等等。
学习和灵活运用软件调试技术,不仅可以提高程序员的工作效率,而且有利于提升对代码的感知力和控制力,加深对软件和系统的理解。此外,调试技术是解决各种软件难题的一种有效武器。它直击要害、锐不可挡,相对其它间接方法具有明显的优势。
软件有大美,调试见真功。在寻找银弹的努力还在继续的时候,衷心地希望所有程序员朋友都学会使用调试这把利剑吧,使用它为你披荆斩棘,帮你探索前进。只要你的这把剑依然锋利,那你的软件青春就永远不老。
张银奎,英特尔亚太研发中心高级软件工程师。毕业于上海交通大学信息与控制工程系,长期从事软件开发和研究工作。对IA-32架构、操作系统内核、驱动程序、尤其是软件调试有较深入研究。翻译作品有《数据挖掘原理》、《机器学习》、《人工智能:复杂问题求解的结构和策略》等。写作品《软件调试》将于2007年出版。
修炼SQL
说起SQL,绝大多数程序员对其作用都了然于胸——用来访问数据库嘛。确实,数据是信息系统的核心,没有数据的计算机应用没有任何意义。信息系统中,大量数据本质上就以实体—关系的模式存在,而且RDBMS支持SQL这么简单但表达能力丰富的访问接口,同时还提供了内建的事务ACID特性保证和故障恢复能力——因此,RDBMS理所当然地成为了大部分信息系统的标准数据存储介质。于是,无论使用何种语言开发信息系统,从C/C++、Delphi到Java,从Perl、Python到Ruby,使用SQL访问RDBMS都是我们必须修炼的武功秘籍。
自从1986年以来,SQL演化出了SQL92、SQL99、SQL2003和SQL2006等多个国际标准。目前,一般开发中用到的SQL都涵盖于SQL92之内——其他几个更晚提出的标准主要关心面向对象、OLAP支持和XML支持等高级特性。正如其名称所指,SQL是一种语言,它帮助我们与特定的对象进行沟通,让“她”了解你的意思,“她”经过各种各样的“思考”,正确、高效地分析处理你的语言,然后“她”给你各种反馈。不同的语言,区别仅在于语法和沟通对象的不同:自然语言用于与人沟通;机器语言用于与机器沟通;高级语言用于与编译器沟通;而SQL则用于与RDBMS进行沟通。
沟通有两个层次,一层是让对方理解你的意思,另一层则是让沟通更加有效地进行。我们修炼SQL当然也有两个层次,一层是让RDBMS理解你要干什么,另一层则是让RDBMS能够更加高效地执行你的语句。
修炼任何武功秘籍的第一步,当然是将整个秘籍铭记于心,并且能够依样画葫芦地进行修炼。同样,为了让RDBMS理解你的意思,必须首先掌握以下这些标准的SQL语法,并写出正确的SQL语句:
· 数据定义语言(Data Definition Language,DDL)指令:CREATE/ALTER/DROP SCHEMA/TABLE/INDEX/VIEW等;
· 数据处理语言(Data Manipulation Language,DML)指令:SELECT、INSERT、UPDATE、DELETE等;
· 事务控制语言(Transaction Control Language,TCL)指令:BEGIN、COMMIT、ROLLBACK;
但是,掌握了这些就够了么?看一个最简单的问题:
· 如何查询表user中的所有数据?SELECT * FROM user;
· 如何删除表user中的所有数据?
○ DELETE * FROM user是个坏方案;
○ DELETE FROM user是较好的方法;
○ 但TRUNCATE TABLE user则更佳。
追求完美的人,不但要写出功能正确的程序,还要写出“漂亮”的程序。在完成修炼的第一步以后,真正的修炼即将开始,笔者权且抛出几块砖头,以抛砖引玉,同时让跟我一块修炼的同仁们尽量少走弯路,早日得成正果。
如何把SQL写得漂亮呢?我的回答是:“美观”+“高效”。
大家觉得这两条SQL哪条写得好看一些?
SELECT * FROM user WHERE (age < (SELECT AVG(age) FROM user)) AND (name LIKE ‘HU%’) ORDER BY id DESC; select * From User WHERE Age < (SELECT avg(age) FROM user) AND Name like ‘HU%’ Order by Id desc;
砖头1:保持一致的风格。
保持一种书写风格,对于SQL关键字和实体指代(表名、列名、索引名、视图名等)采用不同的大小写方式,并且采用恰当的分段和缩进方式,有助于SQL语句的可读性。请注意,让别人看懂自己写的程序是一个程序员的义务!
砖头2:使用视图。
简单就是美,SQL虽然功能强大,但其设计是非常简洁的。然而,经常看对无数个表进行连接,包含十余层甚至几十层嵌套且重复的子查询SQL语句。很显然,这是SQL查询生成工具生成的。既然作为有追求的程序员,就不该使用这种工具,也不该写出如此“恐怖”的程序。至少请牢记,SQL提供一种强大的武器——逻辑视图(View),恰当地使用可以使代码更加简洁,逻辑更加清晰。
砖头3:恰当地建立索引。
美观固然重要,高效更为关键,切不可“金玉其外,败絮其中”。恰当地设计数据库、使用SQL语句,通常可以使得SQL的执行性能成百上千倍地提高,从而极大程度地节省系统硬件成本和维护开销。试想你用一台一万块钱不到的破笔记本,搞定了一个原来需要1台SUN服务器的数据库应用时,老板如何反应?对!破口大骂以前的程序员,然后给你加工资。为了这个目标,继续修炼吧!
那如何能让你的SQL跑得更快?
建立索引(Index)。常用DBMS中,索引通常默认指B+树索引;例如,B+树索引CREATE INDEX i1 ON user(id, age), CREATE INDEX i2 ON user(age, name)有以下几种功能:
(1)找到满足WHERE子句的行集;
SELECT * FROM user WHERE id = 0 AND age = 25; (i1 有效, i2部分有效) SELECT * FROM user WHERE age = 25 AND id = 0; (i1 有效, i2部分有效)
(2)查询某一列的MIN/MAX值;
SELECT MIN(age), MAX(age) FROM user; (i2有效)
(3)提高排序、分组效率;
SELECT * FROM user ORDER BY age, name; (i2有效, i1无效) SELECT * FROM user ORDER BY name, age; (i1、i2 均无效) SELECT age, name, COUNT(1) FROM user GROUP BY age, name;(i2有效,i1无效) SELECT age, name, COUNT(1) FROM user GROUP BY name, age;(i1、i2均无效)
(4)在执行连接(join)操作时,从内表获得行集;
SELECT u.id, u.name, s.grade FROM student s, user u WHERE s.id = u.id AND s.grade = ‘A’ AND u.age < 25; (i1有效, i2在user作为外表时部分有效)
(5)直接从索引获得结果,而不需要访问实际数据行;
SELECT id, age FROM user WHERE id = 0; (i1有效) SELECT name FROM user WHERE age > 18;(i2有效)
为了使得SQL高效,必须根据SQL的不同创建有效的索引;无效的索引并不能提高性能,反而会在插入、删除和更新索引键值等操作时引起额外的数据访问,从而降低总体性能。
砖头4:勤用EXPLAIN。
如何知道创建的索引是否有效呢?目前,常用DBMS都提供EXPLAIN语句,或是图形化的执行计划查看工具。执行计划会清楚地说明SQL语句执行的每个步骤,同时说明每个步骤将应用哪个索引,并给出估计的执行代价。我们可以通过观察执行计划,了解到实际执行中特定索引有没有被利用,以及是否如自己所想的被利用;同时,如查询计划显示需要对某张表进行全表扫描,或者需要对一个大结果集进行排序,则应当考虑建立索引进行优化。另外,查询计划还能清楚地显示其他与性能相关的信息,如:某一个连接操作使用的是嵌套循环连接(Nested Loop Join)、归并连接(Merge Join)还是哈希连接(Hash Join);是否对子查询进行了提升等。
砖头5:适当地进行数据冗余。
应当承认,冗余数据会使得数据库设计比较难看,且不符合各类范式的要求。然而必须指出,按照范式设计仅适用于数据库逻辑设计——在数据库物理设计过程中,在很多应用(特别是Web应用)中,绝大多数操作是进行数据查询;遵守范式、不做冗余时,往往需要将一堆表进行效率低下的连接查询,而这带来的好处仅仅是程序员在做插入、更新和删除时可以少写几条用于保证数据一致的SQL语句。所以,我建议在进行数据库设计和写SQL语句时,一旦发现需要连接,就应考虑是否可以将部分数据冗余起来以避免连接。当然,决非所有情况下,都应该使得数据冗余——如果对于一张表,插入、删除、更新冗余字段等操作远多于查询操作,那么我们应当尽量避免冗余。
砖头6:慎选DBMS/存储引擎。
注意事务控制和加锁特性。大部分商用数据库最低的加锁粒度都是元组/行一级,属性/列级的几乎没有,部分数据库只有页级锁甚至表级锁;加锁粒度越大,导致并发冲突的可能性就越高。如使用仅支持表级锁的MySQL/MyISAM时,对于同一个表的查询和插入操作可以并发执行;但是,所有更新、删除操作之间,以及更新、删除操作与查询操作都无法并发执行。在查询远多于更新的Web应用中, MySQL/MyISAM可以运行得很好;但是对于更新较多的应用, MySQL/MyISAM几乎是不可用的(当然,MySQL/MyISAM不支持事务,也不支持故障恢复,这对于关键应用是不可接受的)。
砖头7:避免死锁和长事务。
死锁(deadlock)和长事务都是常见的影响性能的问题。标准的死锁场景:一个事务中,查询(SELECT)得到一条记录后,对其进行处理,然后更新(UPDATE)该记录;多个这样的事务并发执行时,将不可避免地产生死锁问题。正确的解决办法是使用SELECT … FOR UPDATE方式进行查询,在查询时就加上写锁或意向写锁。另外,长事务特别是更新型长事务应该尽量避免。
砖头8:不打无准备之仗。
进行准备(Prepare)。DBMS执行一条SQL语句的过程大概是这样的:收到SQL语句;进行词法、语法分析,得到查询分析树;进行查询重写和优化,得到查询计划;执行查询计划,得到查询结果。而DBMS在执行Prepare时,将直接生成对应的查询计划并缓存起来,后续的执行只需要每次用不同的参数重新执行查询计划即可。多次执行同一条SQL语句时,使用Prepare将降低总体开销;特别是,当语句实际执行时间较短时,使用Prepare的效果更为明显。另外,Prepare的另一个好处是可以有效地防止SQL注入攻击(SQL Injection),使得数据库应用更加安全。
以上8块砖头是本人习武多年的一些经验和最基本的技巧;当然,数据库是一个非常复杂的系统级软件,SQL也是一本非常博大精深的武功秘籍,其精髓远非这8块砖头能完全概括。作为SQL程序员,我们应该多阅读各种DBMS手册和书籍,多学习数据库内部构造和实现,多思考各种性能优化方法,这样才能把SQL这么武功练得更加出神入化。
借力编程语言走职业开发道路
多数选择开发生涯的程序员最初都有一个长远的目标,因为这个行业有着太多的传奇,有着太多的风华正茂就羽扇纶巾指点着信息技术这个全球性的巨大产业。不过也正是因为这个行业传奇出现的太多、太快,开发人员总是被各种可能动摇既定目标的因素干扰着,这些因素不仅仅包括那些瞬间创富的精英,还掺杂着自己所倾心的IT巨头所作的各种形式的广告。笔者6年前抛弃已经用了几年的Java转而使用C#不能说很有一定程度上与Anders这位“活广告”有关,当年周围朋友们一致反对并苦口婆心劝说的情形还时常想起。
其实,编程语言是如此之重要,以至于在软件开发领域,任何一位职业化的技术人员都会将编程语言当成自己的利器。它们代表了开发人员对计算机本身的理解与对软件开发工作的执著。同时,建立在编程语言之上的基础也标志着程序员的职业化道路发展到了一个新的阶段。
从小工具做起
虽然选择了C#,不过最初笔者所在的项目组主要开发技术还是基于VC和VB的COM+,为了避免给生产系统带来不可预测的负面影响,一开始的尝试也都是围绕着数据为中心的小工具开发开始。在“核心系统不能依赖于具体数据库厂商”的要求下,所有的工具开发为了保持与应用的一致,要同时支持Oracle、SQL Server(客户端部分还常常涉及本地数据库Access),而且辅助性的业务数据操作又不能绑在其中任意一个数据库上,此外出于同样的目的队列相关的工具开发也要同时支持微软的MSMQ、BEA的MessageQ和IBM的MQ。
虽然需要同时面对开发对象很多,但是在Visual Studio.Net环境下通过互操作加托管代码的封装,相关工具的开发并没有遇到多少阻力,借助离线DataSet的支持,在WinForm环境下可以反复对数据进行多次修改,最后一次性的提交,把对业务系统的影响降低到尽可能的小。工具所处理的任务不仅有“短平快”的小数据量操作,也有批量数据的处理,通过C#方便可订制的事件机制,可以向工具使用者提供更好的操作体验。不仅如此,在跳出了MS IDL之后,借助.Net Framework丰富的类库,新的公共支持性COM+也不断用它完成,并被各类小工具所使用。这段经历不仅让自己树立了信心,更是越发强烈了 “放手做一做”的想法。
第一个公共框架的完成
静止的数据只能算是一个空间占用而已,只有当它真正融入到业务人员的工作,并最好把不同来源的数据关联起来的时候,他们的价值才在使用过程中出现了。笔者所在的一个业务系统开发末期需要把来自各处的数据“串”起来,提供数百个总要修改且又功能各异的查询,两个方案出现了:要么继续用熟悉的COM+开发逐个完成,完成一个查询之后“Ctrl-C”一下,然后不断的在其他地方“Ctrl-V”;要么作个公共的框架,定义一个系统内部的查询框架语言,再作一个编译器解释这个语言。我们最后选择了后者,笔者作为一名实施人员参与了将其他同事富有创新思想的设计用C#编码完成的工作。XML + XSD+ 反射 + Smart Client + 基于配置的设计理念,让整个开发工作很有成效,在这个过程中笔者发现用C#结合.Net Framework对于XML的支持是充分而且简便的。同时,封装的Smart Client一次性的解决了很多以往COM+部署上的繁琐。虽然现在回头再看那个框架存在诸多不尽如人意的地方,不过经历过这个过程的实践之后笔者感到面前打开了另一扇门,一个用C#和XML技术基于整个行业内部(甚至整个Internet)不同数据系统间协同开发的大门,之前这种冲击还有两次——初学C++时发现了继承这个有效手段和刚工作时接受到有关N层系统布局的介绍。在对比了之前用VB调用MSXML的经历之后,笔者彻底把内心的天平倾斜到C#一边。
应用于关键支撑系统
编程语言代表了开发人员对计算机本身的理解与对软件开发工作的执著。同时,建立在编程语言之上的基础也标志着程序员的职业化道路发展到了一个新的阶段。
正如每个行业都有一些关键系统一样,笔者所在的行业也有几个这样的系统,他们之间并不是独立的,相互之间数据交换负载也可以算“繁重”;但除此之外,他们还是整个行业成百个相关系统的数据源,数据神经遍及整个行业。随着业务上数据互联的要求越来越强烈,以往的一些交换性系统或多或少的显露出局限性——不能快速的按需配置,不能适应更多关系数据库产品、队列、XML、WMI查询等诸多数据源类型的互联要求,不能以Plug & Play的方式为交换过程增加更多的控制。新的平台开发同样选择了C#,考虑到行业未来几年的数据交换需要,它需要完成4个基本任务:
· 支持多种异构数据源
· 支持生产环境中以Plug & Play的方式修改数据交换过程的控制
· 支持多处理器的并行处理
· 系统要作为一个“灰箱子”存在,可以随时让运行监控人员获取到他运行中的各类技术性能参数和实时业务吞吐率情况数据。
有了以往的经历,这次的开发虽然对于编码提出了更高要求,但是借助C#一一克服了:
· 异构的数据源在入口处通过各厂商的.Net驱动或者COM互操作解决了,在适配器的帮助下统一转换成为了XML,后续的多步转换工作被一个个基于配置好的“XSD -〉 XSLT -〉XSD”过程处理,出口再根据配置文件中相关XML配置段的要求转换为目标数据源所需的输出形式。
· 一组交换控制被抽象为一个个Step,用C#借助反射和后绑定方式自动的完成了。
· 并行处理借助多线程完成
· 通过委托这个通知渠道,在业务处理过程中嵌入多个性能“探头”,黑色的箱子变成了灰色的。
随着越来越多的应用接入这个支撑系统,C#“双优”的特点凸现出来,一方面可以把以往Win32环境下实现起来繁琐的过程用简单的编码完成(例如:多线程),另一方面作为OO语言又可以快速的根据业务变化更改相关逻辑,尤其设计上采用各种设计模式方法后。
上述工作是任何一种开发语言都能够做到的,笔者只是将自己的经验与各位看官共享,根据每个人从事的行业不同,几种主流的编程语言都应该能够从事上述工作。同时,您也可以根据笔者的经验来不断提高自己对编程语言的认识。
单一语言的局限性
由于软件开发的本质就是处理信息以及数据。一种专门用来处理数据的脚本语言常常是走向更加职业化的必备武器之一。尽管采用C#、Java之类的语言同样也能完成这些数据处理的工作,但是为了能让开发人员变得更有效率,或者是变得更加专业,数据处理语言就需要时时伴随在您的身边。毕竟“尺有所短、寸有所长”,应用类语言对于小批量的数据操作是可以应付的,但是大批量的多关系操作就又是数据操作语言的长项。考虑到效率和维护的因素,应用中不仅要有客户端和逻辑层精心布局的大量组件,同样需要在数据库部分安排一系列存储过程、定制任务,对于需要二次甚至多次数据分发的系统还需要完成各类复制。简而言之,与其用单一的语言以复杂的方式完成工作,不如恰当的根据需要选择适当的语言。
挖潜
需要补充的是您还需要经常针对自己的工具“挖潜”,毕竟学习一门语言是需要周期的,而可以捕捉的机会却相对短暂,新事物出现的时候一般都会很密集的集中在一个特定的语言上,比如设计模式的描述几乎是Java,即便用到了C#语法但骨子里还是Java,游戏开发也一样,普遍使用的都是C++,不过您手中的工具拥有着比Java更丰富的OO语言元素、有更方便使用的GDI+,怎么就不能以更简洁的方式完成这些工作呢?MSDN论坛和开源社区充斥着很多“这个事情我该怎么做”的问题,可是当您对手中的这几件工具有了较为深入的认识后,一个灵动——“我还可以用他们做这些”也许就会成为撬动您职业生涯杠杆的力量。
小结
精通两种语言,对于任何一个开发人员来说,并非必须,但是对于一个专业化程度较高的开发人员来说,又常常是必要的。
所有这些就在您的指尖之下……
王翔,全国海关信息中心开发部高级架构师,从事海关主要广域分布式系统的设计和实施,多次参与各业务系统的优化。此外,作为信息安全工作组副组长,一直致力于应用密码技术和公钥基础设施保障海关业务的安全运行。
掌握一条工具链——打造高效程序员
在计算机世界的发展历程中,程序员扮演着相当重要的角色。我们投入了无数心血,打造着不计其数的工具,让这个世界上的人们可以生活得更加幸福。当然,在这个过程中,我们也为自己提供了一些工具,让自己在为别人服务的同时,也可以体会到技术进步带来的快乐。这就是我们通常称之为开发工具的东西。对于今天的程序员来说,这些工具几乎成了我们生命中不可或缺的内容。
客观的说,仅仅熟练使用开发工具并不能让我们更加优秀,却可以让我们更加高效。
提及开发工具,最先充斥在脑中的多半是类似于Visual Studio、Eclipse、Delphi之类的名字,它们确实是开发工具,但它们并不代表开发工具的全部。开发工具的范围很广,有帮助我们进行建立模型的建模工具,有帮助我们完成代码转换的编译器,有帮助我们排忧解难的调试工具,有推动我们在进一步的调优工具,有扮演时间机器角色的版本管理工具……其实,凡是能够用来在开发中帮助我们的工具,我们都可以称之为开发工具,从这个意义上来说,我们用来编写文档的文本编辑器是开发工具;与人交流的纸笔是开发工具;让自己逃脱繁琐的自动化脚本也是开发工具……
一个关于开发工具的话题,理应从一个我们最熟悉的内容开始,而IDE当之无愧的扮演着这样的角色,因为在我们的生命中,编码是一种我们最熟悉的日常行为。不过,提及IDE的时候,我已经隐约的看到那些习惯于使用VI和Emacs进行开发的人脸上流露出那种不屑的笑容了。其实,IDE只是一个名字,程序员们都知道它是集成开发环境(Integrated Development Environment)的缩写,这个名字明明白白的告诉我们,它是把一些用于开发的工具集成到一起。我们在前面提到的Visual Studio、Eclipse、Delphi是其中的典型,它们开发中最常用的一些工具——比如编辑器、编译器、调试器等等——功能集成到一起,省却了我们穿梭于各个工具之间的烦恼。从这个角度来看,VI和Emacs也算是一个IDE,因为在大多数使用它们的程序员手里,他们已经不再是简单的文本编辑器,而是具备各种功能的超级武器。如果说与通常意义上的IDE有什么差别的话,这样的环境一般是由自己打造的,而不像那些IDE一样,作为一个整体提供。
谈到IDE,我们一般想到的会是一个集编辑、编译和调试于一身的工具。但随着技术的进步,IDE已经越来越强大,远远超出我们心目中的最初形象,越来越多的内容被涵盖到IDE中,从需求分析、业务建模到软件发布,IDE已经逐渐覆盖了软件开发的整个生命周期。某些IDE甚至集成了即时通讯功能,号称为了方便团队成员之间进行沟通。只要与开发能够搭上边,都可能成为IDE的内容。幸亏“集成开发环境”这个名字具有相当好的可扩展性,如此大的变动,这个名字都能恰如其分的将其涵盖。
还是让我们把目光放回通常开发工具所具有的几项最基本的功能。
当一个编辑器用于开发时,它和普通的编辑器最大的区别应该是什么呢?在我的脑海里,这个答案肯定是语法高亮。如果成天与一个具备此功能的环境打交道,对此,我们可能会不以为然,就像空气一样,通常情况下,我们不会注意到它的存在,只有失去它时,我们才会意识到它的重要。没有高亮的语法,编写代码出错的概率一下子就会增加许多,尽管我们不愿意承认,但我们确实可能把诸如关键字这样简单的内容都敲错,高亮的语法很大程度上帮助我们避免了编译之后才发现错误的尴尬,看似简单的功能却在某种程度上帮助我们提高了工作效率。如果你有在一个干干净净的Unix类环境下工作的经历,你会知道我在说什么。
曾经用过Turbo C写代码,后来,过渡到用Visual C++写程序,知道什么最让我惊讶吗?代码补齐,就是当我们写下“.”或是“->”,一个长长的列表出现了。这个功能极大的提高我编程的效率,因为我不必再去担心打错那些天书一样的函数名,不必一点点核对每个参数。
语法高亮和代码补齐如今已经是一个编辑代码的环境所必备的功能,看上去平淡无奇,却着实对于提高工作效率大有裨益。而今的很多开发工具提供了更为完善的编辑环境,比如在很多人熟悉的Eclipse中,有一个叫做“Flying Error”的功能,也就是不必等到编译,它就会把一些语法错误和警告提示出来,这样会节省大量“编译再修改”的时间,极大的提高了编程的效率。和同事开玩笑,现在可以号称编写一次编译通过的代码了。如今这个功能几乎成了Java开发环境的标准配置。
重构,随着Martin Fowler的那本书传遍了世界,也成为程序员的随身技能之一。不过,因为那本书的年龄在计算机世界中已经不小了,所以,难免有些过时的东西。比如,其中需要用单元测试来保证重构的正确性,我不否认单元测试的重要性,但非要用它来保证重构的正确性,那是多年以前的状态。如今很多开发工具已经集成了重构的功能,这个功能可要比那种依靠人编写大量的测试保证变换正确性的方式更为可靠。当然,如果工具不支持,只有选择祈求上天,然后埋头去写测试了。
随着技术的进步,IDE已经越来越强大,远远超出我们心目中的最初形象,越来越多的内容被涵盖到IDE中,从需求分析、业务建模到软件发布,IDE已经逐渐覆盖了软件开发的整个生命周期。
说了不少开发用编辑器特有的威力,而作为一个编辑器,它们本身就拥有强大的能力。这也就是为什么天下尽是GUI的时候,VI和Emacs还有那么多忠实的用户。对于GUI用户来说,VI和Emacs那种手不离键盘便无所不能几乎是难以想象的,事实上,它们就是这样强大。不过,它们拥有一定的门槛,刚刚和它们打交道的时候,它们会表现得难以驯服,但随着对它们熟悉,就会越用越熟练,它们就会逐渐把自己最好的一面展示出来。当然,不管用什么编辑器,我们都要耐心的了解它的脾气秉性,才能让它们发挥威力,我们得到的回报就是工作效率的大幅度提升。
我们都知道,真正让代码为这个世界贡献力量的是编译器,但使用一些IDE,我们往往会忽略后面编译器的存在,对于初涉编程的人尤其如此。还记得自己刚开始写代码的时候,用VisualC++写了为数不少的代码之后,居然还不知道后面真正发挥作用的是个叫做“cl”的家伙。不了解IDE背后的动作,直接导致的结果是不清楚编译器如何运作,没有事便是万事大吉,出了事就是不知所措。通常,我们认为在Unix类环境进行开发的人对工具有着更加深入的理解,因为在那里,一切都是透明的。当我有机会接触到Unix类环境,用VI写程序,GCC编译代码的时候,一切豁然开朗。后来,把Intel的ICC集成到VisualC++的时候,我显得那么心安理得。所以,即便我们面对的是一个巨大的黑盒,了解背后的故事,可以让我们更好的与它们合作。
调试器是一个让人犹豫的话题,因为在很多敏捷人的眼里看来,调试器的动用多半意味着单元测试没有做好,尤其在很多人已经进入了测试驱动开发的时代,一旦牵扯到单元测试的缺失,那就成了值得大家鞭挞的对象。然而,无论如何,调试器都会是开发人员手中的强力工具。提起它,我们最先想到的肯定是它的基本功能,也就是给程序找毛病的过程。虽然调试更多的是一个考验耐性和经验的过程,但好的调试器确实可以起到事半功倍的作用。如果让我来比较GUI和命令行的对应工具,大部分工具我会犹豫不决,那么在调试器这项上,我会坚定不移的站在GUI这边,它同代码的结合要比命令行工具舒服多了。
对很多程序员来说,调试器还是一个很好的学习工具。当我们拿到一段新的代码时,有时仅仅依靠静态的代码阅读无法体会全部的含义,这个时候,我会想到运用调试功能跟随代码一起动态的运行一次,一些看起来不那么清楚的结构——比如Java中的接口和实现的对应——便也一目了然了。
现在,越来越多的开发工具呈现出一种开放的态势。像VI和Emacs这样原本叫做编辑器的工具,之所以可以成为开发环境,本身就是靠着自己强大的扩展能力。而正在软件开发世界扮演越来越重要角色的Eclipse,更是凭借着自身的扩展能力在众多的Java IDE中杀出一条血路。这些开发工具的开放性让我们有了更多按照自己想法去工作的途径,从修改配置到开发自己的插件,我们可以选择自己工作的方式。除此之外,良好的扩展性正是我们在软件开发中所追求的,因此,在用的同时,更多的了解这些工具本身,对于提高我们对于软件开发的认识也是很有帮助的。
在我们程序员的生活中,开发工具扮演着非常重要的作用。熟练的掌握一些工具,是我们成为一个高效程序员的基础。我们要做工具的主人,却不能完全依附于它,只有它能够帮助我们提高工作效率的时候,才是我们的好伙伴。事实上,我们从未放弃对更好工具的追求,这也是新的开发工具层出不穷的原因。在结束这篇关于开发工具的文章之前,送给所有阅读本文的人一句话,它来自我最喜欢的一个开发工具IntelliJ IDEA:Develop with Pleasure!
《SICP》与《Art of Unix Programming》
说起程序员的武器自然少不了技术书籍,它们就像是拳谱、剑经,虽然不能马上转化为巨大的伤害输出,但假以时日勤以研读,有朝一日成为傍身绝学也是说不定的。不过虽然各类技术书籍汗牛充栋,除去入门时浅显易用的参考和复杂深奥的学术专著,能够让所有程序员常看常新的心法秘籍却还是不多。笔者想来想去,能符合上至八十一、下至一年级的普适心法,也只有两本:《Structure and Interpretation of Computer Programs》和《The Art of Unix Programming》。
《SICP》
《SICP》中译《计算机程序的构造和解释》,是MIT计算机系6.001课程的教材。我曾向很多人推荐过这本书,每每还不忘哭天抢地的嘱咐一句:多买一本留着传家,万一我侄子您儿子不幸也干了计算机,给孩子留点玩意儿(这话说得还真不是夸张,我自己就买了两本,一本自用一本留给我那不知何年何月才能出生的儿子当见面礼)。为啥说它能传家呢?因为这本书讲解了应该如何应对软件开发的根本难点——控制软件复杂性。
Abstraction & Combination
由于计算机本身是一个很简单的装置,它不能直接理解任何的业务需求,因此计算机程序设计的起点就是将一个业务概念或业务操作描述成为计算机可以理解的程序构造块。当然这个过程根据所使用的语言和工具的不同也有所不同。比如对于汇编语言,任何需求都必须被描述为寄存器和操作指令;对于C、Pascal这类面向过程的语言,就是数据结构和针对这个结构的算法;对于Java、C#这类面向对象语言,就是类和对象。这些过程我们可以笼统地将其称为“抽象”(Abstraction)。
当然掌握了基本的抽象技巧仅仅是程序设计的开始,只把业务功能抽象为程序构造块并不能完成软件的开发,还需要合理地将这些抽象组合(Combination)在一起。例如对于面向过程的程序设计,什么是基本的组合方式呢?表达式。抽象算法和数据结构必须在表达式中组合。
result = complex_add(c1, c2); quick_sort(&array);
第一个表达式代表了复数的加合,其中应用了算法complex_add和数据结构complex(c1和c2),第二个表达也应用了算法quick_sort和数据结构array。而对于面向对象,基本组合方式就是消息传递:
c1.add(c2); array.sort();
可以看出,同样的业务,由于使用的语言不同,抽象和组合的方式也不尽相同。但是其基本的Abstraction & Combination的思路还是一致的。因此,我们可以说,掌握一门语言的基本能力就是掌握语言提供的抽象和组合方式。这里大家也可以做一个思考,你常用的语言到底提供了那些抽象?又提供了哪些组合方式?然后再对照SICP中讲解的抽象方式。应该就可以明白Alan Kay所谓的“Smalltalk之后只有商业成功的语言而无真正突破的语言”是什么含义了。
Conventional Interfaces
Conventional Interfaces是解决软件复杂性的一个重要且基本工具,但是说句实话,在非函数式背景的语言里,要想招到一个恰当的例子还真是一件比较难的事情。我只好找一个Unix里的例子来说明什么是conventional interface了。Unix的基本概念里有一个很有用的假设,就是文件模型,任何设备都会以文件的形式出现。那么文件就成为了一个不折不扣的conventional interface,很多工具都是以文件为基础进行设计的。很多原本没有打算在一起工作的软件,因为有了conventional interface可以很好的协调工作在一起。例如,我有一个几乎每日必用命令行命令,他会帮助我生成一个脚本,将新增的文件添加到SVN里:
svn stat | grep \? | sed 's/\?/svn add/' > svn_add
我使用了三个工具,svn,grep和sed。SVN是Subverion的客户端工具,Grep是Unix/Linux下一个常用的文本查找工具,同样SED是Unix/Linux下一个常用的字符替换工具。这三个工具是独立开发的,有着各自独立的用途。但是由于他们都使用文件,这一conventional interface,因此可以在彼此毫不知晓内部结构的前提下,组合在一起使用。
《SICP》中给出了一个有名的conventional interface,就是list,大家可以去看看。顺便说一句,当今最流行的conventional interface就是XML,但可惜的是,比起Unix的file mode,它的易用性差得远了。再多说一句,面向对象的conventional interface应该是对象(Smalltalk和Ruby都是这样的),但是主流面向对象语言(Java,C#)在这里作为很差,它将类型和接口作为conventional interface,易用性同样差得很远很远。
Meta-Linguistic Abstraction
Meta-Linguistic Abstraction还有另外一个名字,就是叫做语法糖。对于任何抽象我们都可以通过对语言本身的扩展使之成为语言的一部分。例如,我们可以在Java里模拟Ruby对象的行为,也就是在运行期查找对象的行为,而不依赖于静态类型:
class RubyObject { Map<String,RubyFunction> functions; public RubyObject call(String name, RubyObject... args) { functions.get(name).call(args); } } rubyObj.call("method1", value1, value2);
那么我们就可以写一个解释/编译器,将这个行为封装成语言的一部分,比方说我们可以扩展出新关键字dynamic,用于声明对象使用动态语义:
dynamic class Person { }
这也是解决软件复杂性的一个基本方法。但是对于大多数人而言可能过于复杂了。这里我们也就不细说了。
综述一下,《SICP》对于解决软件复杂性给出了三个基本方法,并且反复使用这三个基本方法构造出了非常复杂的应用(第四章中的解释器,第五章的虚拟机和编译器)。实际上我个人一直认为这三个基本方法,应该是所有程序员都应该掌握的基本技能,也是面对软件复杂性的基本武器。
《The Art of Unix Programming》
《The Art of Unix Programming》不同于《SICP》,它更像一本程序员每日行为手册,里面一次又一次地告诉我们那些事情是每天都应该做的,那些是程序员的基本操守。限于篇幅,我只介绍一点。
清晰胜于机巧
当然这个原则并不是绝对的,如果你不需要team,只用闷头写一些只有你自己需要看懂的代码,那么你就可以进行释放你的想象力,写着华丽而精绝的代码。但如果不是这样,这就是你必须恪守的基本法则,是所有专业程序员道德的底线,是程序员的基本操守。在这个行业里,好代码的最高标准只有两条:可以工作、team里所有人都看得懂。除了这一点,《The Art of Unix Programming》还指出,清晰性是可维护性和扩展性的根源,看懂并理解之前的代码是任何维护和扩展的前提。这里扯一句,实践上来说,Pair Programming是达到代码清晰的一个绝佳实践。
除了基本操守之外,《The Art of Unix Programming》中还有两点对于今时今日的中国程序员来说,格外的有价值。那就是Open Source和Hacking Game。
Open Source
Open Source是目前国内软件社区很热的一个主题,不过太多人都没能真正理解Open Source的精神,而仅仅是希望通过open source实现自己悲壮的英雄主义理想。《The Art of Unix Programming》则以Unix的发展历史为背景,详细的论述了Open Source的产生、发展和内在的精神气质。它将Open Source描述为一种community-driven的软件开发模式(套用现在的buzzword,就是software development 2.0)。因此open source的真正意义在于形成一个有着相同或相似价值趋向的开发社区。以整个社区的想法将软件发展壮大。因此,open source本质上就是一种团队行为,而不是个人能力的体现。反观国内的开源事业,大部都是放卫星式的开源。我想我们都应该好好读一读《The Art of Unix Programming》并反思一下,是否真的理解了开源的基本含义。
Hacking Game
Hacking Game说白了就是玩。革命老将王朔说,搞文学关键在于得你玩文学,不能让文学玩你。搞程序也一样,得你玩代码,不能让代码玩你。玩的基本形式就是hacking(这里的hacking和我们所谓的"黑"其实不是一个概念,它只是一种极端的“get things done”的行为。一般是指为了解决一个具体问题去修改别人的代码)。因此,在Unix社区和open source社区内,普遍都将hacking当作一种高度复杂的脑力游戏。《The Art of Unix Programming》将hacking game称为玩家文化,玩家文化是评价一个社区活跃与否的客观标准,hacking game玩得越好的领域,创造力也就越高。
说了真么多,并不是代表《The Art of Unix Programming》只是泛泛而玄虚的哲学探讨。实际上,在技术上,这本书是可以对照着《SICP》来看的,它以Unix开发中实际的案例为《SCIP》中讲解的三个基本技术作了最完美的实例说明(私下里我常常称之为《Pragmatic SICP》或《SICP by examples》)。这是本难得的艺术与技术俱佳的好书。
最基础的数据结构
任何一个受过专业训练的程序员,对“数据结构”这门课程中涉及到的各种数据结构都不会感到陌生。但是,在实际的编程工作中,大部分的数据结构都不会用到,而且也许永远都不会用到。造成这种现象的原因有二:一是根据80/20法则,常用的数据结构只会占到少部分;二是计算机语言往往已经对常用的数据结构进行了良好的封装,程序员不需要关心内部的实现。
虽然如此,深入地理解基本数据结构的概念和实现细节,仍然是每一个程序员的任务。这不仅是因为,掌握这些知识将有利于更加正确和灵活地应用它们,而且也是因为,对于语言背后的实现细节的求知欲是一个优秀的程序员的素质。
本文将讨论实际编程最经常使用的三种数据结构:字符串、数组和Hash表,比较它们在不同语言中的实现思路,并涉及它们的使用技巧。
字符串
严格地说,字符串(string)甚至不能算作一种单独的数据结构,至少在C语言中,它仅仅是某种特定类型的数组而已。但是,字符串在实际使用中是如此重要,在不同语言中的实现又差异颇大,因此,它值得被作为一种抽象数据类型单独进行讨论,并且在我们讨论的三种结构中排名第一。
最经典的字符串实现,应该是C语言中的零终结(null-terminated)字符串。如上所述,C风格的字符串实质上是一个字符数组,它依次存放字符串中的每个字符,最后以零字符(’\0’,表示为常量null)作为结束。因此,字符串占据的空间比它实际的长度要多1个单元。在实际应用中,它常以数组或字符指针的形式被定义,如下例:
char[] message = “this is a message”; char* pmessage = “an other message”;
C语言中,字符串并不是一种独立的数据类型,也没有提供将字符串作为一个整体进行处理的运算符。对字符串的所有操作,实际上都是通过对字符数组的操作来完成。
试想一个函数,功能是求C风格字符串的长度。实现的思路是:设置一个计数器,然后用一个指针遍历整个字符数组,同时对计数器进行累加,直到字符串结束(指针指向了null)。实际上,C语言中的strlen函数也是这么实现的。这种方式看上去非常合理,但是在处理一个非常大的字符数组时,会遭遇到严重的性能问题。如果一个字符串长达数M甚至更大,那么求其长度的操作,需要执行数百万次甚至更长的循环。更糟糕的是,由于这个结果没有被缓存,所以每次求长度的操作都会重复执行这些循环。
C风格字符串的另一个缺陷是,它不会自动管理内存。这意味着,如果字符串的长度超出了数组能够容纳的范围,程序员必须手动申请新的内存空间,并将原来的内容复制过去。这种方式不但产生了大量无谓的工作,而且是无数臭名昭著的溢出漏洞的原因。一个最简单的例子是,当一个程序要求用户输入一个字符串时,如果用户输入的字符串的长度大于程序设定的缓冲区的长度,将会导致溢出,最终程序会崩溃。
针对C风格字符串的这些缺陷,新的语言进行了相应的改进。作为C的直接继承者,C++语言在标准库中提供了一个基础字符串的实现:std :: basic_string。它封装了大量常见的操作,例如取长度、比较、插入、拼接、查找、替换等等,并且能够自动管理内存。例如,由于C++支持运算符重载,因此C++字符串可以使用运算符直接进行运算,而不需要调用strcpy函数。另外,C++字符串也提供了与C风格字符串进行转换的功能。基于强大的模板机制,C++字符串将字符串的实现和具体的字符类型分离开来了。下面是两种最常见的字符串类型:
typedef basic_string<char> string; // 定义了ansi类型的字符串 typedef basic_string<wchar_t> wstring; // 定义了 宽字符类型的字符串
不幸的是,由于复杂的历史原因,许多C++方言(例如Visual C++和Borland C++Builder)都提供了与标准字符串不同的字符串实现。这些字符串实现各有长处,但是将它们和C++标准字符串以及C风格字符串进行转换,又成为了一项令人头疼的工作。
Delphi对字符串的改进基于另外一种思路。在Delphi中,字符串仍然是一种基本类型,而不是类。它的实现方式也是字符数组,不同于C风格字符串的是,在数组的头部增加了两个32位整数存储空间,分别用于存放字符串的长度和引用计数。通过前者可以方便地获得字符串的长度,而不需要进行无谓的遍历操作。后者实现了COW(Copy on Write)技术,这种技术的效果是:当字符串被复制时,并不会复制其内容,而只是建立一个新的指针,指向原有的字符串,并在引用计数上加一。当字符串被删除时,引用计数减一,当引用计数为0时,字符串的内存将被释放。只有当对字符串进行写入操作时,才会建立一个新的字符串并复制内容。这些工作是由编译器自动完成的,程序员完全可以象使用C风格字符串一样使用Delphi风格的字符串,只是效率大大地提高了。
Java和C#中的字符串,是一个封装了常见操作的类,这一点和C++类似。一个特殊之处(往往导致经典的性能问题)是,无论是在Java还是在C#中,String类都是不变(immutable)的。也就是说,String的内容不能够被改变,如果代码试图改变一个String对象的内容,实际的结果是建立了一个新的String对象,并抛弃旧的对象。如下例:
String s = ""; for (int i = 0;i < 10000;i++) { s += i + ", "; }
结果是建立并抛弃了10000个String对象,这在性能上的开销是惊人的。为了避免这种情况,应该使用StringBuilder对象,它可以改变其内容。(C#一直使用StringBuilder。Java从1.5开始引入StringBuilder以部分替代StringBuffer,它们的主要区别在于线程安全性。)如下例:
StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.append(i + ","); }
数组
从抽象数据类型的意义上来说,一维数组(array)的定义是:具有相同数据类型的若干个元素的有限序列。
在C语言中,数组意味着一块连续的内存空间,按顺序存放着若干个相同数据类型的元素。可以通过下标来访问数组中的元素。如下例:
int a[10]; // 定义一个int型的数组 for (int i = 0;i < 10;i++) { a[i] = i; // 赋值 }
在C语言中,数组名事实上是一个指针(指向该数组的第一个元素),因此所有通过数组下标完成的操作,都可以通过指针来完成。通过指针来访问数组,效率上比数组下标要高,而且更加灵活,例如,指针可以进行偏移量的运算,甚至可以进行绝对地址的存取。
C语言中的数组没有越界检查,这意味着,程序员可以访问数组最后一个元素以后的地址,或者第一个元素之前的地址(例如,a[-1]、a[-2]这种形式是合法的)。在某些情况下,这是一种有用的技巧,但大多数情况下是一场灾难。C语言的数组也不支持自动增长,如果数组的长度发生了变化,程序员必须手动处理所有关于申请和释放内存的工作。
C++提供了C风格的数组,同样不支持越界检查和自动增长。但是,C++(至少是Stroustrup博士本人)建议,应该尽量使用STL中的容器作为替代品,一般是vector。Vector基于面向对象和模板技术,构建了一个强大而复杂的类,实现了如下特性:高效率的自动内存管理:按任何顺序访问、插入和删除元素;越界检查,但同时也提供了不进行检查的访问方式,以照顾性能上的考虑;基于运算符重载技术的运算符支持;基于迭代器的漫游机制;与数据类型无关的算法支持;等等。相对于C风格的数组,vector是一种更高抽象层次上的序列概念。它对大量常用的功能进行了封装(例如,对内存的直接操作),同时又尽可能地照顾了效率和可移植性(例如,在自动扩充时通过缓存机制来提高效率)。这也正是C++语言对C语言进行改进时的指导思想。
Delphi也支持C风格的数组,但提供了越界检查。另外,Delphi还提供了一种动态数组(Dynamic Array),可以在运行时通过SetLength函数动态地改变它的大小。事实上,SetLength函数就是对内存管理操作的一种封装。类似于C++中的vector,Delphi也提供了两个可以自动增长的容器:TList和TObjectList,前者用于存放无类型的指针,后者用于存放对象。由于Delphi不支持模板机制,所以TList不会自动释放指针所指向的内存,它只会维护指针自身占用的内存(TObjectList能够在销毁时自动释放元素所占用的空间,如果它的OwnsObjects属性被设置为True的话)。一种常用的解决方法是,编写一个针对具体类型的包裹类,使用一个作为私有数据成员的TList对象来管理指针,并手动编写申请和释放内存的那部分代码。这样总比C语言中的情况要好得多。
Java也支持加上了越界检查的C风格数组,但它提供的类似容器更为引人注目。Java将序列(List)作为一个单独的接口提取出来,并提供了两个实现:ArrayList和LinkedList。从名字就可以看出来,前者是通过数组来实现的,后者则通过链表。由于都实现了List接口,二者可以支持同样的基本操作方式,不同的是,ArrayList在频繁进行随机访问时有效率上的优势,而LinkedList在频繁进行插入和删除操作时效率较优。实现了List接口的类还有Vector和Stack,但是它们在Java 1.1以后就被废弃了。由于LinkedList可以在序列的头尾插入和删除元素,它可以很好地实现Stack和Queue的功能。
Java在1.5以前的版本中也不支持模板,因此List(以及其他的容器)接受Object类型作为元素。由于在Java中所有的类都派生自Object,所以这些容器能够支持任何对象。对于不是对象的基本类型,Java提供了一种包裹类(wrapped class),它能够将基本类型转换成常规的类,从而获得容器的支持。这和Delphi的解决思路异曲同工。
Hash表
作为一种抽象数据结构,词典(Dictionary)被定义为键-值(Key-Value)对的集合。举例来说,在电话号码簿中,通过查找姓名,来找到电话号码,这个例子中姓名是key,电话号码是value。又比如,在学生花名册中,通过查找学号,来找到学生的姓名,这个例子中学号是key,学生的姓名是value。词典最常见的实现方式是Hash表。
Hash表的实现思路如下:通过某种算法,在键-值对的存储地址和键-值对中的key之间,建立一种映射,使得每一个key,都有一个确定的存储地址与之对应。这种算法被封装在Hash函数中。在查找时,通过Hash函数,算出和key对应的存储地址,从而找到相应的键-值对。相对于通过遍历整个键-值对列表来进行查找,Hash表的查找效率要高得多,理想的情况下算法复杂度仅为O(1)(遍历查找的复杂度为O(n))。
但是,由于通常情况下key的集合比键-值对存储地址的集合要大得多,所以有可能把不同的key映射到同一个存储地址上。这种情况称为冲突(collision)。一个好的Hash函数应该尽可能地把key映射到均匀的地址空间中,以减少冲突。Hash表的实现也应该提供解决冲突的方案。
Hash表是一种相对复杂得多的数据结构,从底层完整地实现一个Hash表,也许超出了对一个普通程序员的要求。但是,由于它是如此重要,了解Hash表的概念和掌握使用它的接口,仍然是一项必不可少的技能。
C语言中没有提供现成的Hash表,但是C++提供了优秀的Hash表实现容器hash_map。象STL中的其他容器一样,hash_map支持任何数据类型,支持内存自动管理,能够自动增长。特别地,hash_map通过模板机制,实现了和hash函数的剥离,也就是说,程序员可以定义自己的hash函数,交给hash_map去进行相应的工作。如下例:
hash_map <string, int> hml; // 使用默认的Hash<string>函数 hash_map <string, int, hfct> hml; // 使用自 定义的hfct()作为hash函数 hash_map <string, int, hfct, eql> hml; // 使用自 定义的hfct()作为hash函数,并且使用自定义的eql()函数比较 对象是否相等
Java定义了Map接口,抽象了关于Map的各种操作。在实现了Map接口的类中,有两种是Hash表:HashMap和WeakHashMap (HashTable在Java 1.1以后已被废弃)。后者用于实现所谓“标准映射”(canonicalizing mappings),和本文讨论的内容关系不大。HashMap接受任何类型的对象作为键-值对的元素,支持快速的查找。如下例:
HashMap hm = new HashMap(); hm.put("akey", "this is a word"); // 使用两 个字符串作为键-值对 String str = (String) hm.get("akey"); System.out.println(str);
HashMap和hash函数也是剥离的,但使用了另一种思路。在Java中,根类型Object定义了hashCode()和equals()方法,由于任何类型的对象都派生自Object,所以它们都自动继承了这两个方法。用户自定义的类应该重载这两个方法,以实现自己的hash函数和比较函数。如果这两个函数没有被重载,Java会使用Object的hashCode()和equals()方法,它们的默认实现分别是返回对象的地址,以及比较两个对象的地址是否相等。
在PHP中,数组和Hash表合而为一了。从语法上看,PHP中并没有Hash表这样的容器,而只支持数组。不同的是,PHP中的数组不但支持使用数字下标进行索引,而且支持使用字符串下标进行索引。换句话说,PHP中的数组支持使用键-值对作为数组的元素,并且可以使用键来进行索引(键必须为integer类型或string类型)。而且,PHP中的数组支持自动增长和嵌套。如下例:
$arr = array(1 => 12, "akey" => "this is a word"); echo $arr[1]; // 得到12 echo $arr["akey"]; // 得到"this is a word"
PHP没有提供自定义hash函数的接口。由于它不接受integer和string以外的类型作为键,这一点事实上也没有必要。
结束语
当接受这篇文章的约稿时,我认为这是一项比较简单的工作。因为这三种数据结构实在是太基础了,所以我甚至怀疑是否能够写出足够长的篇幅。很快我就发现了自己的错误。光是字符串就够写一本书的。
在撰写本文的过程中,我回顾了学习过的大部分编程语言,重温了许多经典书籍中的相关章节,启动了各种IDE编写测试用例。我接触到了大量未知的领域,至今我仍然在猜测许多问题的实现细节。这从另外一个方面说明了基本数据结构的重要性:即使在我们最熟悉的事物中,也隐藏着极为深刻的原理。
一个企业在面临人才甄选的时候,如果把台阶放得太低,不但对企业自身会产生极大的影响,那些身在其中的软件技术人员也会失去斗志。这绝不是耸人听闻,纵观当今各大软件巨头,无论是微软、甲骨文、IBM、SAP还是后起之秀Google,都号称自己的员工是全世界最优秀的。这些企业不但建立了高高的企业门槛,也为自己的员工创造了最好的条件。为了享受这种条件,踏平这些企业的门槛无疑成了众多膜拜软件巨头拥蹙们首先要完成的目标。为此,我们挑选了一组面试题,尽管他们有些并非来自软件巨头,但通过这一组小文章,也能够让您理解每一个面试,每一次机会对程序员有什么要求;每一道面试题,背后代表了怎样的实力。
面试题大解析之一——另一种哲学
题目:
Linux的启动进程次序怎样?
Windows中的GDI与DirectDraw的区别在何处?
如果按照有些面试题,学习计算机很可能会误入歧途。去年,最流行的Java面试题是关于设计模式的。有些公司的面试题非常简单,甚至让你只需要简单说出二十三个设计模式即可。难度稍大一点的,则是让你写其中一种的代码。笔者始终不明白设计模式对于Java程序员的必要性何在。这有点让我想到Windows中的MFC,其实无论是MFC还是设计模式,这种框架性的知识结构,对于程序设计都是有用的。但是它能起到的作用通常与程序员对它们的理解程度相关。不过在计算机领域,更重要的不是这些框架性的知识。在本篇面试解析文章的两道题,就比设计模式这类内容更加值得一问。这两道问题除了能够让面试官快速了解面试者对计算机系统的认识外,还能通过面试者的回答了解对软件系统认识的观点。
这里是第二道面试题的简单答案:
当向显示缓存写入数据的时候,就会显示出图像。DirectDraw的作用就是获得缓存区地址的,并且还能创建一个虚拟的缓存区内存。DirectDraw直接操作显示图像,GDI则与它不同,当GDI显示一个图像的时候,不用管显示卡是什么模式,只要设置好颜色,发一个绘制命令就行了。如果是一个15色的图,用GDI显示图形,它会将相应的色彩进行转换,把它转换成显示器支持的,这个过程还需要一点时间。DirectDraw没有这个过程,它只是提供一个方法,直接向显存写数据。
也许这个答案稍微有些长,但如果要把两者的区别讲清楚,这种程序的答案还是非常有必要的。其实,DirectDraw与GDI的区别客观存在,这与一个开发人员写程序的设计模式没有任何关系。往往在计算机中实际发生的事情,比我们对它们的看法更重要,这也是为什么有人说写程序就是调试的理由。不断有人发现了最佳、最有效的组织程序的方式,但是这些最佳的组织方式,与计算机内实际发生的物理过程相比,总是较为次要。因为所有的理论都必然建立在计算机客观发生的物理过程之上,在面向对象的程序设计语言中,比如C++非常喜欢用动物的行为当成例子。尽管程序员可以用C++写一个狼吃羊的程序(这确实很有趣),但在计算机内却并不存在一只狼或羊。计算机中存在的是CPU、内存、显示器等等物理的设备,当他们接通电源的时候,就会按照你编写的代码进行运转。法国哲学家萨特,有一句名言,存在先于本质,在程序设计领域也是有效的。当然,存在也许是琐碎的,程序设计的捷径往往是对这样琐碎而真实的存在的理解,而非各种宏大美妙的构架或框架。
这是笔者对这一类型的面试题目的理解。当然,也许有很多程序员能够回答第一题,但却回答不了第二题。这并不要紧,俗话说,知道得多不如知道的深。正如一位编程高手在他的书中所指出的,只要你一直关注一个问题,一直深入下去,你就有可能达到一个新的境界。
面试题大解析之二——枚举与递归
题目:
请用Java或C编写一个程序,从N个整数中找出最大的一个。
请用Java或C编写一个程序,找出两个整数的最大公约数。
第一道题是一个测试初级程序员的题目,所以题目被设计得尽能简单,可是这样简单的题目,来面试的应聘者仍然有不少做不出来。
其中有一位女程序员在完成第一道题时用到了起泡排序,而且还写错了。她临走的时候抱怨,为什么不考J2ME或者BREW。也许她说得不错,我们需要一个游戏移植者,按说应该考J2ME或者BREW,可是如果你不能正确地完成类似第一题这样的基本考题,让面试官如何相信,你能够掌握J2ME、BREW或者别的什么程序技术?
另一个应聘者可能更具代表性。他是全副武装来的,自带了笔记本电脑,和不错的、能玩复杂手机游戏的手机。面试开始之前,他给我们展示了他移植的游戏,以及他自己开发的游戏。也许这个小伙子日后会成为高手,但现在的他是否是开发高手很值得怀疑。如果我从那些他移植和开发的游戏当中要看出这点并不容易,这些不足以成为判断依据。他用了一页纸写好了他的答案:先用起泡排序,再用二分查找。从答题的问卷上,我们看出他在回忆,回忆他曾经见到过的这方面的知识。
其实对于这类的简单问题,并不需要系统地学过相关知识。如果他曾经系统地学习过这些内容——起泡排序或者二分查找,并充分理解了这些算法的基础知识,他应该明白这道题目并不需要这些。临走的时候,面试官对他说:“你很喜欢游戏编程,又有一定的这方面的知识,只是编程能力确实无法达到我们要求。解答这道题是一个基本要求,因为我们需要一个思考者,而不是具有很多计算机知识的人。”
接下来让我们看看第二题。这道题从某种角度上体现了出题者的怪癖,他像许多人一样认为数学好与年轻是成为好程序员的必要条件。当然,面试官是很难用长达两三个小时的考试来鉴别一个人数学才能的,因为即使那样也有可能出错。不过既然能够有机会对面试者进行当面的测试,一道简单的题目将是发现一个数学爱好者的好机会。尽管完成这道面试题和用一个单词测试一个人的英语水平一样,可能会出现较大的偏差,但是很显然面试官还是想尝试一下。
来面试的两位程序员已经有几年的编程经验,他们的水平无疑已经超过了面试官要找的初级程序员水平。然而,两道简单的题目让面试官看到了很多经验背后的东西。非常遗憾,他们都没有带来惊奇,并很正确地用比较笨的方法完成了这个题目:找较小的那个数,逐步减一,直到完成最大公约数的循环。当然,这种方法也是一种可以信赖的计算方法,但仅限于初级程序员。这二人因为其他原因,并没有被录用,除了因为选择了效率比较低的算法,其它方面也被限制了。
我们最后录用的,是所有面试人员当中唯一想到用欧几里得算法的人。尽管在采用这种方法的时候,他出了一点小错,但最终我们还是录用了这位程序员。事实证明,面试官的赌博没错,他来公司不久就能很快上手工作。毕竟在人群中,能记得住欧几里德算法的多少有些奇特,这也正是我们所需要的。前不久,他对面试官说,你的if/else的写法有问题,正确的写法应该是这样的……
这两道题用到的正好是算法设计的两个基本思路,枚举和递归。其中第二道的解是古希腊人欧几里得的杰作,据说是人类历史上的第一个非平凡算法。它在数论理论上的地位非常重要,中国数学家华罗庚也曾经专门写过关于它的文章。当然,在算法设计上,它还有很多变化,在此就不详述了。第一道题是算法分析中最简单的一个例子,它的时间复杂度与它的问题规模正好线性相关,用符号表示就是O(n)。
两道题的答案如下 :
int findmax(int a[],int N){ for(int max=a[0],i=1;i<N;i++) if(a[i]>max) max=a[i]; return max; } int gcd (int m,int n){ if(n==0) return m; rerurn gcd(n,m%n); }
面试题大解析之三——无声的声音
题目:
· 小明和小强都是张老师的学生,张老师的生日是M月N日,2人都不知道张老师的生日是下列10组中的一天,张老师把M值告诉了小明,把N值告诉了小强,张老师问他们知道他的生日是那一天吗?
3月4日
3月5日
3月8日
6月4日
6月7日
9月1日
9月5日
12月1日
12月2日
12月8日
小明说:如果我不知道的话,小强肯定也不知道。
小强说:本来我也不知道,但是现在我知道了。
小明说:哦,那我也知道了。
请根据以上对话推断出张老师的生日是哪一天?
· 有一个100层高的大厦,你手中有两个相同的玻璃围棋子。从这个大厦的某一层扔下围棋子就会碎,用你手中的这两个玻璃棋子,找出一个最优的策略,来得知那个临界层面。
这也是两道有名的面试题目,我以前对这样的智力测试性的题目不感兴趣,但是在采访过吴文虎教授以后,我的想法有些改变。同时,也多少理解了为什么世界上许多计算机著名厂商喜欢这样的题目。其中第一道题,有一种称呼叫爱因斯坦谜题,没有人考证过是不是爱因斯坦首先发明的,不过,借用一下爱因斯坦的名字也无可非议。我来解释一下第二道题,因为这道题比较容易说得清楚。
这个问题的关键是,你可以先想象一个更简单的,这也是计算机程序设计中常有的策略,比如说在数学中,解三次方程、四次方程的时候,你可以找到一种途径,把它化成二次方程,到了五次方程,一般的情况却找不到这样的途径,那就麻烦了一点,因此伽罗华创造了伟大的群论。不过,我们的问题没有那么复杂,你可以想象一个围棋子时的策略。如果只有一个围棋子,你一开始就从一百层开始测,那么如果围棋子碎了,你就无法判断从1到99层,哪一层是临界点。很显然这样是不行的,走不通,就走相反的方向,从第一层开始测,如果碎了,这一层就是临界点,如果没碎,可以逐层再往上试,直到试到临界点那层。这是只有一枚围棋子的情况.如果有两枚围棋子供你选择,那么其中第二枚围棋子的行为,有点像只有一枚围棋子情况下的围棋子的行为,它总是从最下层逐层向上试。
在有两个围棋子的情况,可以给我们一个先探一下路子的机会,比如我们先从10层开始测起,如果碎了,我们再用剩下的围棋子,从第一层开始试起,逐层向上,这样加起来,用11次测试就够了,如果没有碎,我们可以上升10层,到20层再做这样的测试。如果碎了,我们可以判断,临界层一定在10层到20层之间,再用剩下一枚围棋子逐层测试,这样可以测到100层,但是这还不一定是最优方案。最优的方案是从第13层开始测试,总的测试次数不会超过14次,这样的策略与以下的数学事实相关:
14+13+12+11+10+9+8+7+6+5+4+3+2+1=105>100
这个问题,看上去与计算机技术没有关系,它既不是算法(不直接是),也不是物理机制。它代表了一种思想,也就是面向问题的思想。在我与吴文虎教授的采访中,我多次诱使他说一些著名课程,比如操作系统或数据结构之类,但是我注意到,在整个采访中,他甚至连一个计算机内的技术名词都没有提到过,他说的是任务驱动法。仙风道骨的吴文虎教授和江湖刀客的梁肇新先生,在这个地方竟然是不约而同的一致,面向问题,通过解决问题发现规律。在战争中学习战争。我个人认为在面试中考算法,或者计算机的某些物理机制是合理的,有意义的,它们可以引导我们对计算机技术的理解,不过它们还是太知识化了。问题解决竟然是一种前算法和前物理机制的,所谓“道可道非常道”,这是无声的声音。
他们是谁
1.他是谁。他1912年6月23日出生于英国伦敦,很小的时候就显现出出众的数学天赋,在他上过的中学有这样的传说:这个孩子有些奇怪,他会不假思索的说出一些数学难题的正确答案,却不知道怎样把它们证明出来。1931年,他考入皇家学院,大学毕业以后留校任教,不到一年的时间,他就发表了好几篇很有分量的论文,并且被选为皇家学院研究员。为此,他的母校宣布放假半天以示庆祝,连那时如日中天的罗素都致函邀请他讲学,大约也就是这个时候,他聆听了希尔伯特的演讲。这是一个固执的形式主义数学家,他尝试用一套形式体系把数学统一起来。这个尝试后来被证明是失败的,但却启发了这个英国数学怪才,他发现了形式系统的另一个意义。1937年,他发表了那篇著名的论文《可计算性及其在判定问题中的应用》。后来,这篇论文被认为是现代计算机原理的开山之作。再后来,人们用他的名字命名他所描述的计算机模型。
他是谁。他是美籍匈牙利人,他的智商被认为可以和爱因斯坦相比。1903年出生于匈牙利的布达佩斯,从小聪明过人,兴趣广泛,读书过目不忘,据说他一生掌握了七种外国语,六岁时能用希腊语与父亲交谈,对读过的书籍或论文,他可以一句不差的复述。他在数学的很多领域做出过卓越的贡献。第二次大战以前,他主要从事的是集合论的研究,他的公理化体系奠定了公理集合论的基础。1944年他发表了奠基性的重要论文《博弈论与经济行为》,论文阐述了博弈论的纯粹数学基础,以及博弈应用的详细说明。不过,人们认为,他的最大贡献是关于计算机科学与计算机技术的。现在一般认为ENIAC机是世界第一台电子计算机,1945年,他开始参与ENIAC,在共同讨论的基础上,他制定了全新的存储程序通用电子计算机方案。这个方案奠定了新机器由五部分组成,它们包括:运算器,逻辑控制器,存储器,和输出与输入设备,并且使用二进制,数据与指令都是用二进制,这简化了计算机结构,从而大大提高了计算机的运行速度。
他是谁。幼时的他喜欢组装飞机模型、收音机电路、无线电控制航模,甚至电报装置。1932年进入密歇根大学时,他就毫不犹豫地选择了电机工程专业,1936年毕业时,他在学校的广告栏瞥见了麻省理工大学提供半工半读的机会,他便直捣MIT,一面攻读工程硕士的学位,一面为范尼佛·布什工作。他给布什看着微分分析仪,这是由大大小小齿轮滑轮组成的,占据了大半个房间的机械系统,被视为那个时代最强大的计算装置。在研究中,他发现这个分析器复杂的电路控制是上百个相联系的开关实现的,由此,逐渐意识到电路开关能实现逻辑运算,他写出了自己的论文《中继和交换电路的符号分析》。在论文中,他说明了逻辑代数的真值与假值可以用数1和0表示。在这篇文章中,他画出了一个能进行二进制运算的电路图。这一发现的深远意义在于预示着判断不再是人类特有的能力,它激发了人工智能领域的灵感。1942年在贝尔实验室的工程师拉尔夫的帮助下,他开始考虑如何使用同一种方式传输不同的信息,以及通信信道在受瓶颈限制和噪音干扰下,如何才能正确地传递讯息。1943年他找到了答案,并且在1948年,在《贝尔系统技术》上他发表了那个划时代的论文《通讯的数学理论》,这个理论后来成为通信工程学的基础,同时也激发了现代纠错码技术和数据压缩技术。
他是谁。1930年5月11日生于鹿特丹,他的父亲是一位化学老师,他的母亲是一位数学老师,这种充满科学气息的家庭对他产生了深刻的影响。1951年,他参加了剑桥大学开设的,学习电子计算装置程序设计课程,在那里,他给Van Wigingaarden写信,询问自己的知识是不是足够。后者很快给他回信,肯定了他的教育背景,并邀请他到阿姆斯特丹作为一名程序员为自己工作。从此,他开始了自己的程序员的生活。他是最早提出“goto”有害的人,他解决了著名的哲学家进餐问题,他还发明了以自己的名字命名的最短路径算法。他同时是Algol60的设计者和实现者。1999年,在69岁的时候,他结束了作为教授的职业生涯.。他说:“对于我而言,计算机科学的第一个挑战是如何把命令维持在有限条内,然而巨大的分立的宇宙是复杂的,相互缠绕着的。第二个也是同样重要的挑战,是如何传授解决第一个问题的方法”。
他是谁。1938年1月10日生于美国威斯康星州,他在模式方面的辨别和操作能力,在少年时代就显现出来。他参加过一个拼字比赛,而且以4500个远远超过一般的2500个成绩获得头奖。上大学以后,他才开始喜欢数学。当时一个爱出难题的数学教授提出一个特殊的问题,并说哪个学生能解决这个问题,就能立刻得“A”,他在一次等公共汽车的时候,发现了答案,结果他果然得到了那个“A”,并且得到了逃课的权利。1962年,还是一名研究生的时候,他就已经开始了自己的计算机编程工作,他曾经做过个人咨询,为不同的机器编写编译程序,这些工作最终促成了3000页的手稿,而这些手稿也就是那本巨著《计算机编程艺术》的蓝本。到1976年,这本书已经卖出超过一百万册。
他是谁。他生于1950年8月11日,狂热于电子学,在担任工程师的父亲启发下,他从小就喜欢修拆电子产品。1968年,他开始在科罗拉大学学习电子工程学。1971年6月,从学院退学后,他和朋友用当地整容公司丢弃的零件组装成他们自己的计算机,在向当地新闻记者演示的时候,闪光灯使得电源着火。大约在这个时候,他认识了史蒂夫·乔伯斯,他们两个创办了他们的第一个商业企业,两人沿街叫卖一种叫做蓝匣子的东西,它能通过模拟电话公司的信号,免费拨打电话。在经历了几次险些被警察抓住和幸免于持枪行凶者的威胁以后,他们关掉了这个公司。1976年,他完成了自己的计算机设计方案,他的朋友乔伯斯发现此中具有巨大的商业价值。一开始他们曾说服各自的老板,生产他们的产品,可是都遭到了拒绝,他只好卖掉自己喜爱的计算器,他的朋友则卖掉了运输车,这样他们再一次创建了自己的公司,并且成功地推出了自己产品,他们给自己起了一个名字叫苹果。
他是谁。他1955年10月28日,出生于美国西北部,华盛顿西雅图,他的父亲是律师,他的母亲是教师。他的同时代人,即使在那样的年龄,也能看出他的与众不同。酷爱数学与计算机,那时,保罗艾伦是他的朋友。保罗喜欢《大众电子》,他则更喜欢阅览商业周刊,他很早就看出计算机技术会给他带来经济上的利益。1972年5月,这个书生气十足的男孩把他的第一个软件卖给了他就读的学校,得到了4200美元的报酬,中学毕业的时候,他就对他的同学说,他要在25岁以前挣到他的第一个100万。1973年他被哈佛录取,可是他很少认真听课。他把精力都用在计算机爱好上了,那时有个人宣布制造了个人电脑,他和他的朋友感觉到这是一个机会,他们打电话过去,承诺三个星期的时间,做出为那台机器使用的BASIC,他们日夜苦干,最后成功了。从这里开始,他和他的朋友们构造了一个举世惊异的软件帝国。
2.他是图灵,他是冯·诺依曼,他是申农,他是狄克斯特拉,他是诺斯,他是沃兹尼亚克,他是比尔·盖茨。
同时,他也是那个上海的,因为还不起房债而跳楼的人;他也是深圳的那个因为连续加班,疲劳过度而致死的工程师。可以想象,他一定在想自己可以在年轻的时候多学点东西,而那个公司或许正在把他培养成爱国主义楷模。他还是那个哆哆嗦嗦站在面试官前,突然被问到整数有多少位,而不知所措的大学生……
3.美国后现代小说家,约翰·巴思写过一本叫做《词语》的小说,小说中的人物的名字全是缩略语。整个故事:ABC杀了BCD,BCD爱上了DD,与BBB做生意,他们在一个叫做DFGH的城市参加了选举,最后ABC莫名其妙的死去。这是一个缩略语的时代,我们无法辨别自身,我们为一个符号而存在,我们自己就已经成为符号;甚至连符号也不是,而只不过是一些符号的代指。也许正是这个理由,本期的策划开始了对程序员,优秀程序员也就是程序高手,甚至他们所可能掌握的技能进行了一番剖析。程序员已经不再是申农时代的意义,一个数学家,他可能意味的是一个精明的商人也可能是一个最最普通的工程师,未来不久或许还包括卡车司机,即便是在这样的假设下,我们依然列出了一份清单:
· 数组、字符串与哈希表
· 正则表达式
· 调试
· 两门语言
· 一个开发环境
· SQL语言
· 编写软件的思想
如果你对这些都不屑一顾,那就继续锻炼自己的独门暗器吧。因为这份清单的意义,正如上面所说,不过是为了反省自身。在更广大的人群中,它或许仅仅是一枚石子,能激起一些水浪。
4.其实没必要伤感,沃兹尼亚克在上个世纪八十年代的一次飞机事故后,患上失忆症,他现在做的是教小学生怎样使用计算机。他说,我的一生的梦想就是成为一名工程师、一名教师,我现在做到了。狄基克斯特,这个计算机界的哲学家,2002年病故。
老一代的侠客渐渐退隐,新一代的英雄,出现在地平线上,他们已经或正在准备着拔出自己的刀剑。
(2007年第3期)