MrA 2011-07-14
我们都知道,C++中最重要的概念——类,了解了类之后,已经可以开始做些编程方面比较高级的应用——设计程序,而不再只是将算法变成代码。要说明如何设计程序,有必要先了解何谓编程思想。建议大家阅读这一系列的文章,供大家参考。
编程思想
编程,即编写程序,而之前已经说过,程序就是方法的描述,那么编程就是编写方法的描述。我知道如何到人民公园,然后我就编写了到人民公园的方法的描述—— 从市中心开始向东走400米再向右转走200米就是。接着另一个人也知道如何去,但他编的程序却是——从市中心沿人民东路走过两个交叉口,在第三个交叉口处右转,直走就能在右手方看到。很明显,两个程序不同,但最后走的路线是相同的,即大家的方法相同,但描述不同。
所谓的编程思想,就是如何编程,即编写程序的方法。这也是为什么不同的人对同一算法编写出的程序不同(指程序逻辑,不是简单的变量或函数名不同),不同的人的编程思想不同。
如果多编或多看一些程序,就会发现编程思想是很重要的。好的编程思想编出的程序条理分明,可维护性高;差的编程思想编出的程序晦涩难懂,可维护性低。注意,这里是从程序的易读性来比较的,实际出于效率,是会使用不符合人脑思维习惯的编程思想,进而导致代码的难于维护,但为了效率还是会经常在程序的瓶颈位置使用被优化了的代码(这种代码一般使用汇编语言编写,算法则很大程度上是数学上的优化,丢弃了大部分其在人类世界中的意义)。
本系列一直坚持并推荐这么一个编程思想——一切均按照语义来编写。而语义是语言的意义,之前说它是代码在人类世界中的意义。比如桌子,映射成一个结构,有桌脚数、颜色等成员变量,那么为什么没有质量、材料、价格、生产日期等成员?对此有必要说明一下“人类世界”的含义。
世界
我们生活在一个四维的客观物理世界中,游戏中的怪物生活在游戏定义的游戏世界中,白雪公主生活在一个童话世界中。什么叫世界?世界即规则的集合。比如客观世界中,力可作用于有质量的物体上,并进而按照运动学定律改变物体的速度;电荷异性相吸同性相斥;能量守恒等,这些都是对客观世界这个规则的集合中的某些规则的描述。
注意它们都只是规则的描述,不是规则,就好像程序是方法的描述,但不是方法。即方法和规则都是抽象的逻辑概念,各自通过程序和论调来表现。程序就是我们要编写的,而论调就是一门理论,如概率论、运动学、流体力学等。而前面所说的游戏世界,是因为游戏也是一系列规则。
同样,童话世界也是由一系列的规则组成。如白雪公主能吃东西,能睡觉,并且能因为吃了毒苹果而中毒;魔镜能回答问题等。
那么就算了解了世界这个概念又怎样?有什么用?前面说了本系列是推荐按照语义来编写程序。而算法是基于某些规则的,如给出1到100求和的算法是(1+100)*100/2,这里就暗示已经有那么些规则说明什么是加减乘除,什么是求和。即一个算法一定是就一个世界来说的,它在另一个世界可能毫无意义。因为算法就是方法,是由之前说的命令和被操作的资源组成,而命令和资源就是由世界来定义的。
前面说根据算法写代码,其实是先制订了一个世界来做为算法展示的平台。如之前的商人过河,其就在如下的一个规则集上表现的:有一只能坐两人的船可以载人过河;有三个商人和三个仆人在河的一边;河的任意一边仆人数多过商人数商人就会被杀。这是对过河问题所基于的世界的严重不准确的描述,但在这过于抽象并没什么好处,只用注意:上面的商人和仆人不是现实世界中的商人和仆人,他们不能吃饭不能睡觉不能讲话,甚至连走路都不会,唯一会的是通过坐船过河来改变自身的位置。当某一位置(即河的某一岸)的仆人的实例多于商人的实例时(且商人的实例至少有一个),则称商人被杀。上面的描述暂且称为商人仆人论,它是对过河问题所基于的世界的一个描述。
另一个人却不像上面那样看待问题。河有两个岸,每个河岸总对应着两个数字——商人数和仆人数。有一个途径能按照某个规则改变河岸对应的两个数字(就是坐只能坐两人的船过河),而当任何一个河岸所对应的仆人数多于商人数时(且商人数不为零),则称商人被杀。此人没有定义商人和仆人这么两个概念,而只定义了一个概念——河岸,此概念具有两个属性——商人数和仆人数。这是另一个论调,暂且称为河岸论。
什么意思?上面就是对商人过河问题所基于的世界的两个不同论调。注意上面论调不同,但描述的都是同一个世界,就好像动力学和量子力学,都是对客观世界物体之间作用规则的描述,但大相径庭。算法总是基于一个世界,但更准确点的说法应是算法总是基于一个世界的描述,而所谓的设计程序就是编写算法所基于的世界描述,即论调,而论调其实就是问题的描述。
现在考虑前面说的商人仆人论和河岸论,它们都是同一世界的描述,但前者提出两个名词性概念——商人和仆人,各自具有“位置”这个状态和“坐船”这么一个功能以及“商人被杀”这个动词性概念;后者提出一个名词性概念——河岸,具有“商人数”和“仆人数”两个属性和“商人被杀”及“坐船”两个动词性概念。
在此,说后者比前者好,因为后者定义的名词性概念更少(即名词性概念比动词性概念更容易增加架构的复杂性,因为其代表了世界中东西的种类,种类越繁多则世界越复杂,越难以实现),虽说不一定更容易理解,但结构更简单。
易发现,所有的论调都可以只由“名词性概念”和“动词性概念”组成,其中前者在数学中就是数、实数、复数等,后者是加减乘除、求导等,它们都被称作定义。在《游戏论》中,我将前者称为类,而类的实例就是方法中被操作的的资源,后者称为命令。
而在方法中,前者是资源的类型,后者是操作的类型。一个论调,提出的概念越少,结构就越简单,也就越好。但应注意,就电脑编程来说,由于电脑并不是抽象的概念,而是存在效率因素的,因此基于前述的好的论调的算法而编出的代码的运行效率并不一定高。
因此,所谓的程序设计,就是设计算法所基于的论调,而好的程序设计,就是相应的论调设计得好。但前面说了,效率并不一定高,对此,一般仅在代码的瓶颈位置另外设计,而程序的整体架构依旧按照之前的设计。随着程序的日趋庞大,清晰简明的程序架构越显重要,而要保持程序架构的简明,就应设计好的论调;要保持架构的清晰,就应按照语义来编写代码。下面,介绍如此风靡的面向对象编程思想来帮助设计程序。
何谓对象
要说明面向对象,首先应了解何谓对象。对象就是前述的“名词性概念”的实现,即一个实例。如商人仆人论中有商人和仆人两个“名词性概念”,其有三个商人和三个仆人,则称有六个对象,分别是三个商人的实例和三个仆人的实例。应注意对象和实例的区别,其实它们没有区别,如果非要说区别,可以认为对象能够没有状态,但实例一定有状态。
那么什么叫状态?还是先来看看什么叫属性。桌子有个属性叫颜色,这张桌子是红色的而那张是绿的。人有个状态叫脸色,这个人的脸色红润而那个的惨白。都是颜色,但一个是属性一个是状态,什么区别?
如果把桌子和人都映射成类,那么桌子的颜色和人的脸色都应映射成相应类的成员变量,而两者的区别就是桌子的实例在主要运作过程中颜色都不变化,其主要用于读;人的脸色在人的实例的主要运作过程中可能变化,其主要用于写。什么叫运作过程?类映射的是资源,资源可以具有功能,即成员函数,当一个实例的功能执行时,就是这个实例的运作过程。
桌子有个功能是“放东西”,当调用这个成员函数时,其中会读取颜色这个属性的值以判断放在桌子上的东西的颜色是否和桌子的颜色搭配协调。人有个功能是“泡澡”,其可以使相应实例的脸色从惨白向红润转变。但桌子也有个功能是“改变颜色”,调用它可以改变桌子的颜色。
按照前面所说,颜色是属性,应该被读,但这里却在实例的运作过程中对它进行了写操作。注意前面说的是“主要运作过程”,即桌子的目的是用来“放东西”,不是“改变颜色”。如果桌子这个概念在其相应世界中主要是用来改变其颜色而不是放东西,此时桌子只不过是一个能记录颜色值的容器,而这时桌子的颜色就是状态,不是属性了。
有何意义?属性和状态都映射为成员变量,从代码上是看不出它们的区别的,但它们的语义是有严重区别的。属性是用来配置实例而状态是用来表现实例。在面向对象编程思想中,只是简单地说对象是具有属性和功能(也被称作方法)的实例,这在编写的程序所基于的世界比较复杂时显得非常地孱弱,而且就是对“属性”的错误理解,再加上“封装”这个词汇的席卷,导致出现大量的荒谬代码,后面说明。
属性和状态的差别导致出现所谓的无状态对象(在MTS——Microsoft Transaction Server中提出,称作Stateless Component,无状态组件),这正是对象和实例的差别——对象是实现,因此可以是一个抽象概念的实现;实例是实际存在,不能是抽象概念的实现。这在 C++代码上就表现为没有成员变量的类和有成员变量的类。如下:
struct Search { virtual int search( int*, int, int ); }; Search a, b; int c[3] = { 10, 20, 5 }; a.search( c, 3, 20 );
这里就生成两个对象a和b,它们都是抽象概念——搜索功能的对象。注意结构Search没有成员变量,因为不需要,那么a和b的长度是多少?由于可能出现下面的情况,一般的编译器都将上面的a和b的长度定为一个字节,进而&a就不等于&b。
struct BSearch : public Search { int search( int*, int, int ); }; Search *p; BSearch d; p = &a; p = &b; p = &d; p->search( c, 3, 5 );
注意从代码上依旧可以称上面生成了Search的两个实例a和b,BSearch的一个实例d(即使实际上它们根本不存在,逻辑上大小为零),这也就是为什么之前说它和对象没有区别,仅仅有概念上的微小差别。
应注意前面提到的无状态对象并不是说没有成员变量的类的实例,只是没状态,并不代表没有属性。如前面的BSearch可能有个属性 m_MaxSearchTimes以表示折半搜索时如果搜索m_MaxSearchTimes那么多次仍没找到,则BSearch::search返回没找到。
虽然这里BSearch有了成员变量,但就逻辑上它还是一个抽象概念。由于属性和状态的实现相同(都通过成员变量),因此要实现无状态对象需要一些特殊手段,由于与本系列无关,在此不表。
前面的a和b有区别吗?为什么要有两个实例?“搜索功能”按照之前说的语义不是更应该映射为函数?为什么要映射成没有成员变量的类?上面的用法在 STL(Standard Template Library——标准模版库)中被使用,做了一些变形,称作函数类,是一种编程技巧。