现在的位置: 首页 > 综合 > 正文

大道至简第二章

2014年02月15日 ⁄ 综合 ⁄ 共 5918字 ⁄ 字号 评论关闭

第一节 是懒人造就了方法

战国时期的李冰凿了一座山。

《史记》中的“蜀守冰,凿离堆”,说的是李冰在成都做太守的时候凿出了离堆。一说是李冰将都江堰附近的玉垒山凿了一个大口子,叫宝瓶口,而凿的石头就堆成了离堆。另一说,则是李冰的确凿了一座“(溷)崖”,但是是在沫水,亦即今天的大渡河。

在哪里凿的山,是史学家都说不清楚的事。但的确凿了一座山,而且方法是“(因)其崖崭峻不可破,(冰)乃积薪烧之”。

我们已经看到事物的进化了。《列子·汤问篇》里的愚公要“碎石击壤”,而李冰就已经懂得“积薪烧之”了。

会有人说愚公是“碎石”,但史书中并没有说他“碎石”的方法究竟是“斧钺以凿之”,还是“积薪以烧之”。但想想在那个时代,如果有人懂得了烧石头这个方法,哪有不立即载文志之,永世传承的。

再说了,愚公嘛。愚者怎么会呢?这还需要分析吗?需要吗?

所以愚公会凿,而李冰会烧。那李冰又是为什么会用“”这种方法来碎石的呢?如果李冰也像愚公那样日复一日地督促着他的团队凿石开山,那他一定没有时间来学习、寻找或者观察;当然也不会发现“”这种方法可以加快工程进度,使得一大座山在短时间内就被哗啦哗啦地给“”掉了。

要知道李冰的团队可是成百上千人,要修堰筑坝,还要“凿离堆”,当然还要吃喝拉撒睡。所以李冰如果忙起来的话,他必然是“受命以来,夙夜忧叹”,必然食难下咽,睡无安枕。反之,李冰一定是个闲人,可以闲到没事去看火能不能把石头烧爆。

在这么大的工程里,如果有一个人会闲到看火烧石头,那他一定很懒。那么多事堆着不去做,去看烧石头,你说他不是懒是什么。



正是一个懒人造就了“烧石头”这个“碎石”的方法。愚公太勤快了,勤快得今天可以比昨天多凿一倍的石头。或许在愚公的项目计划案的首页里就写着朱批大字:“吾今胜昨倍许,明胜今倍许,而山不加增,何苦而不快。”但是越发勤快,愚公将越发没有机会找到更快的方法。

 

人的精力终归是有极限的。提出新的“方法”,解决的将是影响做事成效的根本问题。而愚公可以多吃点饭,多加点班,但突破不了人精力的极限。

记住,在两千年前的某一天,闲极无聊的李冰下厨给夫人炒了一个小菜,他突然发现垒灶的鹅卵石被烧得爆裂开来,遇水尤甚。从此《史记》上就记下了“蜀守冰,凿离堆”,而《华阳国志》上则记下了他做这件事的方法“积薪烧之”。

第二节 一百万行代码是可以写在一个文件里的

早期写的程序,都是将代码打在穿孔纸带上,让计算机去读的。要让计算机读的纸带当然是连续的,这无须多讲。其实我也没有那样写过程序,其中的苦楚我也不知道。

后来有了汇编语言,可以写一些代码了。这时的代码是先写在文本文件里,然后交给一个编译器去编译,再由一个链接器去链接,这样就出来了程序。

第一个写汇编的人,写的可能是有名的“Hello World”程序,那个程序写在一个文件里就行了。所以后来就成了习惯,大家都把代码写到一个文件里。在早期的汇编语言里,GOTO语句用得是非常非常频繁的,将一条语句GOTO到另一个文本文件里去,既不现实也不方便。所以大家习以为常,便统统地把代码写到一个文件里。

再后来出现了高级语言,什么C呀,Pascal呀之类的。既然大家已经形成习惯了,所以很自然地会把一个程序写到一个文件里。无论这个程序有多大,多少行代码,写到一个文件里多方便呀。

直到如今语言发展得更高级了。可是程序员的习惯还是难改,一旦得了机会,他们总还是喜欢把代码写到一个文件里的。

好了,有人说我是想当然尔。嗯,这当然是有实据的。记得Delphi 1.0版发布的时候,全世界一片叫好声。连“不支持双字节”这样的大问题,都不影响它在华语地区的推广。然而不久,爆出了一个大BUG!什么大BUG呢?Delphi 1.0的编译器居然不支持超过64KB的源代码文件!

这被Fans们一通好骂。直到我用Delphi 2.0时,一个从VB阵营转过来的程序员还跑过来问我这件事。好在Delphi 2.0改掉了这个BUG,这让当时我的面子上好一阵风光。

64KB的文件是什么概念呢?

1行代码大概(平均)是30字节,64KB的源代码是2 184行,如果代码风格好一点,再多一些空行的话,差不多也就是3 000行上下。

也就是说,在Delphi 1.0的时代(以及其后的很多很多时代),程序员把3 000行代码写到一个文件里,是司空见惯的事了。如果你不让他这样写,还是会被痛骂的呢。

所以呢,按照这一部分人的逻辑,一百万行代码其实是可以写在一个文件里的。不但可以,而且编译器、编辑器等也都必须予以支持。这才是正统的软件开发。

勤快的愚公创造不了方法。这我已经说过了。对于要把“一百万行代码写到一个文件里”,并且查找一个函数要在编辑器里按5 000PageUp/PageDown键的勤快人来说,是不能指望他们创造出“单元文件(Unit)”这样的开发方法来的。

然而单元文件毕竟还是出现了。这个世界上,有勤快人就必然有懒人,有懒人也就必然有懒人的懒方法。

有了单元文件,也就很快出现了一个新的概念:模块。把一个大模块分成小模块,再把小模块分成更细的小小模块,一个模块对应于一个单元。于是我们可以开始分工作了,一部分人写这几个单元的代码,另一部分则写那几个。

很好,源代码终于可以被分割开来了。结构化编程的时代终于开始了,新的方法从此取代了旧的方法。而这一切的功劳,应当归功于那个在按第5 001PageDown键时,突然崩溃的程序师。他发自良心地说:“不能让这一切继续下去了,我一定要把下一行代码写到第二个文件里去。我发誓,我要在编译器里加入一个Unit关键字。”

Turbo Pascal 3.0中才开始有了UsesUnit关键字。在ANSI Pascal标准里并没有它。

汇编语言是面向特定硬件系统的一种助记符,所以只应该存在指令(而不包括语句)。因此有没有GOTO,取决于硬件指令系统的设计,而与这种汇编语言没什么关系。

早期的计算机被用来完成复杂的科学运算,因此不可能真的有人无聊到去写Hello World

第二节 一百万行代码是可以写在一个文件里的

第三节 你桌上的书是乱的吗

几周之前,在一所电脑培训学校与学生座谈时,一个学员问我:“为什么我学了一年的编程,却还是不知道怎么写程序呢”。

我想了想,问了这个学员一个问题:“你桌上的书是乱的吗?”

他迟疑了一下,不过还是回答我道:“比较整齐。”

我当时便反问他:“你既然知道如何把书分类、归整得整整齐齐地放在书桌上,那怎么没想过如何把所学的知识分类一下,归纳一下,整整齐齐地放在脑子里呢?”

如果一个人学了一年的编程,他的脑袋里还是晕乎乎的,不知道从哪里开始,也不知道如何做程序。那想来只有一个原因:他学了,也把知识学进去了,就是不知道这些知识是干什么的。或者说,他不知道各种知识都可以用来做什么。

其实结构化编程的基本单位是“过程(Procedure)”,而不是上一小节说到的“单元(Unit)”。然而在我看来,过程及其调用是CPU指令集所提供的执行逻辑,而不是普通的开发人员在编程实践中所总结和创生的“方法”。

这里要提及CPU指令集的产生。产生最初的指令集的方式我已经无可考证,我所知道的是,CISC指令集与RISC指令集之争在1979年终于爆发。前者被称为复杂指令集,然而经过Patterson等科学家的研究,发现80%CISC指令只有在20%的时间内才会用到;更进一步的研究发现,在最常用的10条指令中,包含的流程控制只有“条件分支(IF...THEN...)”、“跳转(JUMP)” 和“调用返回(CALL/RET)”……

于是CISCRISC(精简指令集计算机)替代了。动摇CISC指令集地位的方法,就是分类统计。

X86系统中,循环是用条件分支(或循环指令)来实现的,而且条件分支指令并不是IF...THEN...,这里用这两个关键字,仅用于说明问题。

正如CISC指令集搅乱了一代程序设计师的思路一样,大量的知识和资讯搅乱了上面向我提问的那位学员的思想。他应该尝试一下分类,把既有的知识像桌子上的书一样整理一下,最常用的放在手边,而最不常用的放在书柜里。如果这样的话,我想他已经在九个月前就开始写第一个软件产品了。

你桌上的书还是乱的吗?

第四节 我的第一次思考:程序 = 算法 + 结构 + 方法

我对程序的本质的第一次思考其实发生在不久前。那是我在OICQ上与Soul(王昊)的一次谈话。

SoulDelphiBBS现任的总版主,是我很敬重的一位程序员。那时我们正在做DelphiBBS的一个“B计划II”,也就是出第二本书。他当时在写一篇有关“面向对象(OOP)”的文章,而我正在写《Delphi源代码分析》。在这本书的初期版本里,有“面向对象”这一部分的内容。

这段对话的确很长。如果你不是非常有经验的程序员,那么不能完整地阅读和理解这段文字是很正常的。部分读者甚至可以跳过这段引文,直接阅读后面的结论。

我们的对话摘要如下。

Soul:我在写书讨论“面向对象的局限性”。

我:嗯。这个倒与我的意见一致。哈哈哈。

“绝对可以用面向过程的方法来实现任意复杂的系统。要知道,航天飞机也是在面向过程的时代上的天。但是,为了使一切变得不是那么复杂,还是出现了‘面向对象程序设计’的方法。”

  ——我那本书里,在“面向对象”一部分之前的引文中,就是这样写的。

Soul:现在的程序是按照冯·诺伊曼的第一种方案做的,本来就是顺序的,而不是同步的。CPU怎么说都是一条指令一条指令执行的。

面向过程是对“流程”、“结构”和“编程方法”的高度概括。而面向对象本身只解决了“结构”和“编程方法”的问题,而并没有对“流程”加以改造。

Soul:确实如此。确实如此。

我:对流程进一步概括的,是“事件驱动”程序模型。但这个模型不是OO(面向对象)提出的,而是Windows的消息系统内置的。所以,现在很多人迷惑于“对象”和“事件”,试图通过OO来解决一切的想法原本就是很可笑的。

Soul:我先停下来,和你讨论这个问题,顺便补充到书里去。

我:      如果要了解事件驱动的本质,就应该追溯到Windows内核。这样就涉及到线程、进程和窗体消息系统这些与OO无关的内容。所以,整个的RAD快速应用程序开发)编程模型是OOOS(操作系统)一起构建的。现在很多的开发人员只知其OO的外表,而看不到OS的内核,所以也就总是难以提高。

SoulOO里面,我觉得事件的概念是很牵强的,因为真正的对象之间是相互作用,就好像作用力和反作用力,不会有个“顺序”的延时。

我:应该留意到,整个的“事件”模型都是以“记录”和“消息”的方式来传递的。也就是说,事件模型停留在“面向过程”编程时代使用的数据结构的层面上。因此,也就不难明白,使用或不使用OO都能写Windows程序。

  因为流程还是在“面向过程”时代。

Soul:所以所谓的面向对象的事件还是“顺序”的。所以我们经常要考虑一个事件发生后对其他过程的影响,所以面向对象在现在而言还是牵强的。

我:      如果你深入OS来看SHE(结构化异常处理),来看Messages(窗体消息),就知道这些东西原本就不是为了OO而准备的。面向对象封装了它们,却无法改造它们的流程和内核。因为OO的抽象层面并不是这个。

事件的连续性并不是某种编程方法或者程序逻辑结构所决定的。正如你前面所说的,那是CPU决定的事。

Soul:比如条件选择,其实也可以用一种对象来实现,而事实却没有。这个是因为CPU的特性和面向对象太麻烦。

我:      可能,将CPU做成面向对象的可能还是比较难以想象和理解。所以MS才启动.NET Framework。我不认为.NET在面向对象方法上有什么超越,也不认为它的FCL库会有什么奇特的地方——除非它们足够庞大。但是我认为,如果有一天OS也是用.NET Framework来编写的,OS一级的消息系统、异常机制、线程机制等都是.NET的,都是面向对象的。那么,在这个基础上,将“事件驱动”并入OO层面的模型,才有可能。

Soul:所以我发觉面向对象的思维第一不可能彻底,第二只能用在总体分析层上。在很多时候,实质上我们只是把一个顺序的流程折叠成对象。

我:倒也不是不可能彻底。有绝对OO的模型,这样的模型我见过。哈哈,但说实在的,我觉得小应用用“绝对OO”的方式来编写,有失“应用”的本意。我们做东西只是要“用”,而不是研究它用的是什么模型。所以,“Hello World”也用OO方式实现,原本就只是出现在教科书中的Sample罢了。哈哈。

Soul:还有不可能用彻底的面向对象方法来表达世界。 因为这个世界不是面向对象的。 是关系网络图,面向对象只是树,只能片面地表达世界。所以很多时候面向对象去解决问题会非常痛苦。所以编程退到数据结构更合理,哈哈。

我:如果内存是“层状存取”的,那么我们的“数据结构”就可以基于多层来形成“多层数据结构”体系。如果内存是“树状存取”的,那么我们当然可以用“树”的方式来存取——可惜我们只有顺序存取的内存。

        程序=数据+算法
——这个是面向过程时代的事。
程序=数据+算法+方法
——在OO时代,我们看到了事件驱动和模型驱动,所以出现了“方法”问题。

Soul:我的经验是:总体结构→面向对象,关系→数据结构,实现→算法。
看来我们对面向对象的认识还是比较一致的。

我第一次提到我对程序的理解是“程序=数据+算法+方法”,就是在这一次与Soul的交谈之中。在这次的交谈中的思考仍有些不成熟的地方,例如我完全忽略了在面向过程时代的“方法”问题。实际上面向过程开发也是有相关的“方法”的。

所谓“面向过程开发”,其实是对“结构化程序设计”在代码阶段的一个习惯性的说法。而我忽略了这个阶段的“方法”的根本原因,是即使没有任何“方法”的存在,只需要“单元(Unit)”和“模块(Module)”的概念,在面向过程时代,一样可以做出任意大型的程序。在那个时代,“方法”问题并不会像鼻子一样凸显在每一个程序员的面前。

在面向过程开发中,“过程(Procedure)”是由CPU提供的,“单元(Unit)”则是编译器提供的(机制)。程序员无须(至少不是必须)再造就什么“方法”,就可以进行愚公式的开发工作了。

如果不出现面向对象的话,这样伟大的工程可能还要再干一百年……

而与“面向对象”是否出现完全无关的一个东西,却因为“过程”和“单元”的出现而出现了。这就是“工程(Engineering)”。


 

 


【愚公移山记:智叟商晋】

京城初将所学的技法投入到工程中去,提高了工效,公输一家很开心。端木启则把粗铁变成了好马,进而居奇得利,也很开心。

 

 

抱歉!评论已关闭.