基于 Ruby GScript 谈谈——程序设计语言的通用框架
目录
基于 Ruby GScript 谈谈——程序设计语言的通用框架
众所周知,市面上的可用编程语言不可胜数,面对如此多的各式各样的程式语言。从其中抽象出几乎所有程序语言所共有的特性或框架对于以后其他语言的学习是很有帮助的,并且也可以让我们更好的理解关于程序设计语言工具,让我们更加熟练的应用。
一、架构思维导图
如上所示我们将编程语言归结成:I/O流与文件系统、基本元素、结构语句、抽象与建模、其他功能与模块。
下面我们就基于 Ruby 沿着上述几个方面开始介绍:
1、Ruby GScript 介绍
Ruby 是一种为简单快捷而设计的语言,非常直观,简单易学。它是完全面向对象的:任何一点数据都是对象,包括在其他语言中的基本类型(比如:整数,布尔逻辑值),每个过程或函数都是方法。一种简单快捷的面向对象的脚本语言。而 RGSS 3 ,全名叫做 Ruby Game Script System 3 。中文叫做 Ruby 游戏脚本系统 3 。
Ruby 之父松本行弘是这样说的:
“在我创造 Ruby 以前,我了解过许多语言,他们从未使我满意过。它们总是和我的期望有差距,要么有些丑陋,要么有些糟糕,要么过于复杂,要么过于简单,因此我想创建属于自己的语言,来满足我作为程序员的需要。我很了解这门语言的目标用户:我自己。令我惊讶的是,世界上很多程序员和我的感觉一样。
“在 Ruby 语言的整个发展过程中,我始终致力于使编程更加快速和简单。Ruby 是作为一门让程序员快乐的语言而设计的。 ”
而魔法书 SICP 中阿兰 • 珀利斯先生是这么说的:“LISP 程序员知晓一切值,但对其代价一无所知。
不过对 Ruby 语言来说貌似没有分号。不过语法糖倒也是不少。
2、实践: Hello world!
第一个程序就是让电脑说出 “Hello World!” !
这句话由两个东西构成:一个是函数 msgbox: Message Box。消息窗口。它的作用就是告诉系统:私要显示点东西!而且要一个窗口!系统会反过来问 msgbox:“你想显示点啥?”于是就涉及到了第二个东西。
另一个就是后面的”Hello World!”,这个用双引号引起来的东西叫做字符串,所谓字符串,就是把一些汉字、数字、字母、符号等等你能想到的和想不到的乱七八糟串在一起。形成的一个东西。这个东西可以被显示在屏幕上。
2.1 字串
大家在 Windows 中应该经常见到一个对话框内显示很多行信息,那个又是怎么实现的呢?这个时候就要涉及到 转义字符(Escape Character)这个概念了。所谓转义字符,是指由符号 \ 开头,后面紧跟一个字母或数字表达出来特殊的一种含义。例如用\n 表示换行,用 \0 表示一个字符串的结尾。
关于常见的转义字符表,大部分语言都一样。
3 元素:常量和变量
就像搭积木一样,我们搭建代码的第一块砖就是本节的标题。我们用量来表示和存储数据。
3.1 常量
首先我们要知道 1 是什么。 1 在计算机程序中叫做常量。常量是指计算机程序中不可变的量。例如:整数常量,如: 1、 54、 10086、 65535。浮点型常量(俗称小数),如: 1.03、 3.14。字符串常量,正则表达式常量、符号常量等
字面表达 我们在程序中书写常量的值得方式就是它的字面表达。常见的常量字面表达如下:
整数 :整数的 10 进制表示就是其字面量。中间可以加入“_”断开数字,会被自动无视。以 0 开头的长度超过 1 位的整数会被认为是 8 进制,而 0x 开头的整数将会被认为是 0x 之后的 16 进制数代表的数值。
浮点数:浮点数由可选的正负号、十进制整数、小数点、十进制整数、可选的指数部分组成。指数部分和浮点数本体一样。 Ruby 是不允许.1 或 1. 这样的小数出现的,不然会被误认。
单引号字符串:单引号字符串由两个成对的单引号括起来,中间的内容就是字符串的值。如果中间需要单引号作为字串的部分,则要写成 \’。如果是 \ 作为字面值则需要写成 \\。
双引号引号字符串 :之前大家都见识过了,带转义的字符串。双引号字符串是可以内插值的。如果中间有#{XXX},那么解释器会把 XXX 的值算出来插入到那里去。双引号字符串可以跨越多行,但是除非你在行尾打上一个 \,不然字符串的内容也会包含这个换行。
标识符标志的常量:我们可以用一个标识符代表某个常量。标识符必须由半角大写英文字母开头,之后可以跟_,半角大小写字母的自由组合。起好名字后,我们这样写:名字 = 值。
常量无法改变,只能赋值给别人。 所谓赋值,就是告诉别人:你就是我这个值。那么,赋值给谁呢? 当然是变量啦
3.2 变量
对应的, 常量只能赋值给变量,顾名思义, 变量可以变,可以加减乘除之后再赋给自己,可以创建,可以扔掉。
变量不想常量一样可以直接写字面值,而必须有一个名字——标识符代表它。标识符格则各个语言大同小异。一般来说如下:
1. 第一个字符必须以小写英文字母或 _(下划线)开头。
2. 第二个字符开始可以使用英文字母、数字或 _(下划线)。
3. 中文和非空白全角什么的能不用就不用。
此外,变量名不能和系统关键字冲突。下面是 RGSS3 系统中的一部分保留字,不可以作为变量名:
在 Ruby 中 @ 开头的变量属于类的实例;@@ 开头的变量属于类本身; $ 开头的变量是全局变量;小写字母打头的变量是局部变量,只属于定义它的地方。
3.3 变量和字符串的混合输出
变量和字符串的混合输出例子如下:
运行时,你可以任意改变 a 的值,然后显示的值会跟着改变。在字符串中所用到的 #{} 就是嵌入标志,它告诉 RGSS:大括号内的东西是个变量,按照变量输出。这个就是变量和字符串的混合输出。
4 元素:基本的运算
4.1 数值运算
最基本的运算是赋值运算,然后是变量的加减,c = a+b 。同理,我们还存在着下面这些运算:
+ 加法 - 减法 * 乘法 / 整除或实数除法 % 整数取余或实数取余 ** 乘方
注意的是, Ruby 里面的除法和取余数和 C 什么的不一样,它是对 -1 取整的。
4.2 比较运算
有了数字,当然我们要对它进行比较。比较运算符有以下几种:
== 等于(不要写成单等号);=== case 相等;!= 不等于;< 小于; > 大于; <= 小于等于; >= 大于等于; <=> 基本比较
比较运算会返回布尔值(逻辑真假值) ,即系统预定义的常量 true(逻辑真)和 false(逻辑假)。 但一个特例是 <=>,他会在左边等于右边的时候返回 0,小于返回负数,大于返回正数。
4.3 逻辑运算
那么,如何在多个布尔值之间做与或非运算,得出我们需要的条件呢?
&& 逻辑与; and 逻辑与; || 逻辑或; or 逻辑或; ! 逻辑非; not 逻辑非;
每一种运算有文字和符号两个不同的运算符,差别仅仅在于优先级不同而已。如果要改变 Ruby 判断的顺序,请一定记住要加括
号。
4.4 自运算
就是将等号后面的数直接作用在变量上。像 a = a + (b + c - d) 可以写成 a+ = b + c - d,
4.5 字串运算
5 元素:注释
所谓注释,就是代码中没有任何效果的东西,他们的存在只是为了阅读和理解方便,编译器会直接忽略。Ruby 使用 #,与python 类似。
6 结构:控制语句
6.1 条件语句
就是判断 age > 12 是否成立。如果是的话就输出“adult fee”,否则输出 “child fee。
6.2 循环语句
for 最简单的循环是:
连续弹出 5 个对话框。
while :while~end 结构是当型循环的程序结构。就是“当 XXX 条件下重复执行 XXX,直到条件不满足为止” 的循环结构,故名当型循环。
会显示 15。
7 抽象:函数——第一种范式
7.1 何为函数
函数 :官方一点的定义是:
“函数过程中的这些语句用于完成某些有意义的工作——通常是处理文本,控制输入或计算数值。通过在程序代码中引入函数名称和所需的参数,可在该程序中执行(或称调用)该函数。函数一般都有一个返回值,也可以没有(没有的话, Ruby 自动帮你返回 nil)。 ”
说白了就是把一部分代码拿出去,需要用的时候随时调用,可以反复调用。
我们使用某个东西不是毫无道理的,对不对?比如说:
为什么使用电脑? | ——好玩。可是—— |
你知道怎么用电脑吗? | ——知道。 |
你知道电脑是怎么工作的吗? | ——不知道。 |
不知道它怎么工作,你怎么用它? | ——…… |
事实上我们完全可以用我们不知道细节的东西来工作。也必须这样做。你想,要是制造宇宙飞船的人每设计一个螺钉时,不是考虑“这个是几多少规格的”,而是要先考虑:“这个螺纹的长度是多少,这个槽有多深……”他们还怎么造飞船?
人是不能控制太复杂的东西的。工程学的本质就是通过控制复杂度,来使问题简化。因此现在我们需要一个东西,来让我们关注于问题本身,而不是每一个实现细节。这个东西叫黑盒抽象。那么最基本的抽象方法是什么呢?让一段代码脱离上下文,可以反复执行。这就是函数的本质。
7.2 使用函数
我们其实一直在使用函数。 msgbox、 p、 msgbox_p、print 都是函数。
函数的正规的函数调用写法是: fun ( arg1 , arg2 … , )
fun 是函数的名字,而 argXX 是 XX 号参数。所谓参数,就是每次调用都不一样的、运行时刻才知道值的东西。一个 print函数显然不能只打印一种字符串,那么它怎么知道我们要打印什么字符串呢?靠参数。
像 msgbox和 rand那样的方法已被系统预定义,所以称为系统函数。
7.3 定义函数
函数的定义是从 def 开始到 end 结束……。函数名称的命名方法,基本上和变量有相同的限制。
具体的语法如下所示:
关于函数和参数:形式参数简称形参,实际参数简称实参。这个各个语言大同小异。return语句 return语句后面接一个可选的表达式,函数内的语句执行到这句时,会自动返回调用处,函数的返回值就是return后面的表达式。
默认参数: 定义方法时就已指定且使用时可以省略的参数称为默认参数。在Ruby 中默认参数是在临时参数后面加上符号= 来指定的。
函数重定义: 同名函数可以定义多次,后定义的会覆盖掉先定义的:
函数别名: 嗯,就是换个名字。比较常用的情况是我们希望原来的函数不被完全覆盖的情况下定义重名函数,关键词:alias
8 抽象:简单数据结构
8.1 数组
建立数组: 所谓数组,就是把一堆数绑在一组,起一个名字。于是就叫数组了。后来人觉得只有数字才能放进去很不方便,于是字符串、真假值、对象……统统可以放进数组里了。如语句:j i e = [ 5 4 , 6 , 9 , 1 0 ]
作为整体的数组 记得以前用 Basic 写代码的时候,输出数组总是一件很烦人的事情。动不动就是从第一个一直写到最后一个,一个长长的输出语句,要不然就要写一个循环之类的东西。在 Ruby 中简单的点办法么?有。数组本身就是量,量是可以整体作为参数传入函数、作为返回值返回。如直接 : mesgbox jie
Ruby 中数组状的赋值:
8.2 哈希表
对数组的扩充,用松本原话就是“关联数组”。例子如下:
在哈希表中可以用一些方法来判断某元素是否存在于哈希表中。如下:
9 抽象:类——第二范式
9.1 略看类
什么是类
类是通性的抽象类,表示对现实生活中一类具有共同特征的事物的抽象。比如猫,作为一种生物的统称它代表着一类生物,所以我们可以把它看做一个类,它可以形容所有的猫的通性,比如四个爪子,爱抓毛线球,等等。但是,现在我们并没有真正的猫,还只是个对猫的叙述而已。现在,我自己养了一只猫并给她起名叫喵子,那么, 她就是猫的一个实例,根据类对猫这一物种的描述所创造出来的一只活生生的猫,你看得见摸得着,如果在程序中形容的话,它就是是可操作的,它确实在内存中开辟了属于自己的空间。这就完成了从类到对象的实例化过程。
类封装状态: 注意在编程中类抽象的是一种状态而不是单纯的方法 + 数据。以字符串为例,字符串有值,有长度,有个函数可以把它转化成数字。如果是方法加数据,那好,我们用一个数组来存放字符串的值和长度,定义一个方法把它转化成数字不行么?可是如果真那样做的话,转化方法、值、长度这三者就完全分隔开了。我们操作一个,别的不会因此而自动自发地改变。这就不是动态的状态了。
作为实例的类:在 Ruby 中,类变得更加抽象。现在想象一下,我们要对类这个物体本身也下个定义,类到底是干什么的呢?类就是一个形容别的物体通性的概念,它有着定义方法、确定属性等等的方法,它可以干创造实例之类的事情,那么,我们现在是不是得到了“类”这个物体的通性?那么, 我们可以把“类”也做为一个类看待,叫做 Class 类,前面形容猫的类就是 Class 的一个实例,它是“类”的特指。举个简单的例子,机床可以制造汽车,那么怎么制造机床呢?制造一个制造机床的机床就行了。
9.2 定义类
类自己的定义:类定义的内容就是这一类事物所共同的方法和属性。
标准的语法是:
实例的变量和方法 属于某个类实例的变量需以@开头。每个类都有一个名为 initialize的方法,它的参数就
是实例化这个对象所必须的参数。每当实例化一个新对象是,就会寻找 initialize 方法,然后执行该方法……我们可以用 initialize 方法将预设值加入所有实例变数,这样就不需要在其他地方定义再使用了。如下例子:
注意到上例中的 @age和 @name是不可访问的,因为对实例某个属性的访问可能引起其他属性的变化。18 行和 20 行其实是分别调用了 Person类的 age方法和age=方法来实现读写实例变量的。没有定义写方法的变量,像是 name,就不可访问。那么
对于很多实例变量,他们的访问不需要影响其他变量,我们是不是必须一一都套上外衣呢?不是,如你所想,我们有语法糖,如下,其实该语法糖就是靠元编程实现的。
和之前的那段是一样的,不过简练多了。我想唯一需要注意的一点是: x=(val)这样的方法在类的方法定义的里面是不能直接写 x=val 的。如果确实有这个必要,请用 self.x=val。这里 self是指这个类当前的实例自身。
类的重打开: Ruby里我们可以再打开一个之前定义好类,向这类添加方法……我们一般不要拆开类的定义,因为那样不好理解,拆开类的一个原因是将它们分散到多个文件里,还有后人修改原先定义的类的同时不希望破坏祖先的代码。如:
bye这个方法只属于 luoluo这一个实例。类内方法的覆盖和普通方法的覆盖是一致的,略。
操作符重载、异常结构: 我们希望自己定义的 Vector类能够支持加法、取负和数乘,怎么办?简单,只要定义运算符作为函数名就好了。
raise 是一个内建的方法,用于抛出这个异常。我们在外边可以捕获异常并处理它。如果不处理,异常会一直向外传到解释器那里去,最终弹出一个对话框。但是无论如何, ensure 后的语句确保被执行一次。
9.3 类多态
继承关系和子类: 类与类最基本的关系是继承。
学术的说法: 在类这一抽象层中,并不是所有的概念都是并列的,比如,生物分为五个界,然后下面又分门纲目科属种,这些概念层层递进,同时其共性也在渐渐增多,而这一现象在类中的体现就是——继承。 继承可以让子类继承父类的方法,实例变量等。继承最大的好处就是可以将父类的共性重用。 一个父类不一定只有一个子类,多个子类公用一个父类可以有效降低代码的重复率,更有利于贯彻 DRY (do not repeatyourself) 精神,有助于降低代码的耦合性,提高代码的可重用性。
通俗的说法: 假设我们有木头类,那么桌子是木头,椅子也是木头,所有桌子和椅子具有木头的通性,但又有着更具体的、桌子和椅子各自的通性。因此木头是父类,桌子和椅子是它的两个子类。实例如下:
子类 Cat 可以完全重用父类的 eat方法而不需要自己定义,降低了代码的重复率。
方法可见性: 实例的方法可见性有三种: 公开 (public)、保护 (protected)、私有 (private)。所有的编程语言都
有这样的分类。私有的就像他的塞钱箱, 只有自己(类内部,或者说实例自身)能用,外部不可见;保护的方法有些特殊, 是一脉单传的,本类和它的子类的实例可以访问,外部不可见。私有方法一般是临时调用的子函数或类似的东西,只是为了实现方便,不提供对外接口;至于保护方法,一般是类的继承关系一开始就比较明晰的时候,主要用于不仅自己临时用,子类也会需要用,但是因为比较危险不希望外界随便用的场合。只是 Ruby中加了一条: 在子类实例中通过直接写出名字访问自身的私有方法,编译器默许。在 Ruby 中什么都不写的时候,默认 initialize 是私有方法,其他统统是 public。
子类化:方法覆盖: 但是,仅仅是集成的话是不够的。子类有些功能从根本实现上就是和父类不同的。我们怎么办呢?譬如说吧,不同动物有着不同的发声器官,动物靠它发出叫声。如果子类覆盖掉父类的方法中需要调用继承的同名方法(特别是在 initialize函数中一般都会用到),我们用super关键字。
Ruby的口头规范是:修改自身的函数通常有两个版本:一个作出修改后返回新对象,自身不受影响,没有副作用;另外一个会直接修改自己的相关变量,然后返回新对象。
9.4 模块:特殊的类
现在我们把 module 什么的也当做一个特殊的“类”来处理,其实它就是一个特殊的类,但是它不能被实例化(没有 new 方法)。例子如下:
在当前的用法中,模块仅仅是当做命名空间用的,也就是找一个地方把处理相同功能东西聚在一起,避免重名啊什么的。所以我们只定义模块的类方法,不定义其实例方法。
10 抽象:块和迭代——第三范式初步
10.1 前言
在 Ruby 中被 { } 括起來的就是块——这当然不包括 Hash 。也许你会感到陌生,但实际上块广泛的出现在你的视野之中,只不过是一种隐性的形式。这里指的是 for..in 。实际上这个控制结构就是调用了 each 方法。
那什么是 each 方法呢。它涉及到 Ruby 中迭代的概念。实际上迭代就是调用带块的方法。那什么是块呢。他是一个代码段。注意它并不会直接处理。而是要你去提醒程序处理块。上面的 for...in 对于一个数组来说,可以看成 [ ].each |i| 。处理时会用这个数组的每个元素传送到 i 中,并且处理所带块中的代码段。例如:
在大多数一般情况下,它可以和 for...in 互换。
10.2 过程对象 — Proc
我们并不能直接获取到 Block。这里介绍把 Block 对象化 Proc。这样就可以直接获取一个 Block 了。
生成一个 Proc 有两种方法:
一个是内部函数 proc 。其用法就是:
这个方法会把所带块对象化并返回对应的对象。
另一个是 Proc.new。依然是 block = Proc.new { } 。这里不细谈。
实际上块是可以带参数的,这在 each 里就能看出来。它是用 | | 包含。没有数量上限制。
块和方法
经过前面的介绍,你已经看到块和方法之间是如何相互关联的。你通常使用 yield 语句从与其具有相同名称的方法调用块。因此,代码如下所示:
#!/usr/bin/ruby
def test
yield
end
test{ puts "Hello world"}
本实例是实现块的最简单的方式。您使用 yield 语句调用 test 块。
但是如果方法的最后一个参数前带有 &,那么您可以向该方法传递一个块,且这个块可被赋给最后一个参数。如果 * 和 & 同时出现在参数列表中,& 应放在后面。
#!/usr/bin/ruby
def test(&block)
block.call
end
test { puts "Hello World!"}
这将产生以下结果:
Hello World!
BEGIN 和 END 块
每个 Ruby 源文件可以声明当文件被加载时要运行的代码块(BEGIN 块),以及程序完成执行后要运行的代码块(END 块)。
#!/usr/bin/ruby
BEGIN {
# BEGIN block code
puts "BEGIN code block"
}
END {
# END block code
puts "END code block"
}
# MAIN block code
puts "MAIN code block"
一个程序可以包含多个 BEGIN 和 END 块。BEGIN 块按照它们出现的顺序执行。END 块按照它们出现的相反顺序执行。当执行时,上面的程序产生产生以下结果:
BEGIN code block
MAIN code block
END code block
11、其他
11.1 纯粹的对象逻辑
什么是面向对象?继承、多态、封装。课本如是言。
但是仔细想想我们发现,只有继承的话几乎什么都干不了,封装的话固然可以是代码更容易看懂,但是函数不能封装么?能,不但能,而且封装的很好。我们只需把类转化成 hash 表,然后所有实例方法都对表操作,像是 WinAPI 做的一样。
因此真正重要的是多态。所谓多态就是同一个对象拥有多种状态,既是 A 又是 B。一个子类,既是子类的实例,同时又是他的任何一个父类的实例。因此, 一切对于父类可行的方法、过程、操作,皆可以自动地对于子类成立,而不需写额外的版本。这极大地简化了我们的代码。
Ruby 的多态机制源于 Smalltalk 。 数据是对象,类是对象,凡此一切皆是对象。既然都是对象,那么数据类型就自动的消失了。并不是 Smalltalk/Ruby 没有整型、字符之类的,而是本来就不需要——他们分别是对应类的实例,在传统意义上都是对象类型。而这个对象类型,是包容万象的。
而 Smalltalk/Ruby 的一切运算操作,都可以理解为对对象发送消息。像是 a+b 或者 a.+b ,就是给对象 a 发送符号为 :+ 的消息,带着参数 b ;(你也可以用 send 方法实现这一过程)而 a 收到这个消息之后调用 a 的 +(b) 方法处理。不信请看: (这里为了方便讲解采用了 smalltalk 的代码风格)
这种上世纪 70 年代就诞生了的对象、消息思想是出人意料的简单,但是极其强大。我们甚至可以用对象和消息机制实现逻辑演算(控制结构、循环之类的):
然后:
(不要觉得荒谬, Smalltalk 真的是这么实现的,而且做的很好! Ruby 基本也是!)
那么既然是这样,我们想要实现一个普适的功能,就完全不用考虑什么类型、继承。只需要照做就好了、比如说,我们要实现一个函数,能把一个数组中所有的元素相加求和(假设 Ruby 的数组没那么强大好了):
看到了?我们根本不用管数组里存的是什么。我们根本不需要写多余的代码,就能在不知道数组内部核心逻辑的情况下实现“求总和”这一操作。相应的,调用这方法的一方也不需要知道数组内部的实现机理,只需要放心大胆地把数据带进去就可以了!什么虚函数,什么向上映射,统统不用去管他。对父类成立的方法,对子类就一定成立。甚至于说,连继承关系都可以抛开, 对能处理某消息的对象成立的方法,对一切能处理同样消息的对象就一定成立。这就是传说中的鸭子类型匹配(能嘎嘎叫的就能当成鸭子一样处理,不需额外的代码)。这在C++ 这种限定类型的语言里根本是不可想象的。纯粹的面向对象“生来就是强大的,其次在逐步增加效率;而不是在保证效率的基础上,把各种各样的繁琐复杂的东西堆上去,好让他强大。”
说了这么半天,到底我们为什么要采用多态和面向对象?仅仅是书上说的省代码?不。更重要的是, 符合人类的认知范畴。当我们看到一个椅子,你的第一反应是“这是原子构成的物质”还是“这是椅子”“这是可以坐的东西”?显然是后者。程序员看待数据,本也应该一开始就认识到“这是某类东西”“他可以干什么”,而不是“他的机理是这样的,所以我要这样拿他干什么”。程序不仅仅是一段实现功能的英文,他更是一种逻辑,一种思想,一种描述世界的文字形式。而面向对象和多态给了我们这样的可能,使得我们可以去坐一切可以坐的东西,不管他是桌子、椅子还是床。因此我们的精力就一定程度上放在问题本身,而不是机器实现上了。
不过,我们是不是搞错了什么呢?先不管,先看看多态吧。
11.2 Ruby 的混入继承
不知大家注意到没,生活中的很多对象都不只有一个属性:椅子既可以坐(Furniture 类的实例)又可以烧(Wood 类的实例)。但是 Ruby ,还有其他大多数的语言,都只支持单类继承。也就是说 Chair < Wood 或者 Chair < Furniture 二选一,剩下的即使方法一样,也不能继承!纯生的面向对象—— Smalltalk 类大部分方法是共同的,类层次结构也比较明晰,所以几乎用不到多重继承。然而 C++ 和 Ruby 呢?显然,他们需要 (考虑 istream 类、 ostream 类和 iostream 类) 。
Ruby 不支持。也许你会疑惑为什么?不过我得承认在语言本身偏于杂乱(吸收多家语言杂合)的情况下,这样的选择是正确的。比如说, C 同时继承 A、 B,而AB 有一个重名的方法 (例如 initialize) ,那么运行的时候怎么算?覆盖,还是分立?如果像 C++ 一样选择分立,那么,假设 A、 B 又有一个共同父类 D,那 A、 B中共有的 D 的部分怎么算?依然分立?那样不仅浪费空间,功能上也是不恰当的。比如说 Wood 和 Furniture 都是 Good 类的实例, Good 类有一个 Price(价格) 的变量,那么当 Chair 调用 Wood 的父类的方法修改价格的时候,它从 Furniture 的父类那里得到的价格应该也应该改变。 C++ 为了解决这个冲突又增加了虚继承等机制,不仅使语言内质变得繁琐杂乱,而且增加了额外的开销,还有可能造成其他冲突(最晚辈派生类的初始化问题等)。因此前辈们提倡在 C++ 系列中使用二次封装(就是接口类或纯虚类)或直接继承公共基类而不是
多重继承是有道理的。
然而,这里是重视语言强度胜过内在实现的 Ruby ,从理念上,它应该是尽一切可能使程序员把目光放在问题本身而不是实现上。从语言的哲学上,它支持纯粹的面向对象,没有数据类型差异和向上映射,它也不能支持多类继承不是很不好吗?于是它引入了一个新概念—— MixIn(以下翻译作“混入继承”) 。但是没有引入新的对象,而是把本来只是作为命名空间的模块直接拿来当做混入方法的容器了,这个是相对安全的。
Ruby 中有可比较对象(支持 between? 和各种比较运算符),有可枚举对象(支持 all,any,collect,find 等)。我们如果有一个表,它既可比较(字典序),又可枚举,怎么办呢?可以观察到可比较对象和核心是 <=>方法(类似于 C++ 中的纯虚方法),其他的如 between? 等在该方法实现之后其实现都大同小异;而可枚举对象的核心是 each 方法。
11.3 对象还是消息?类抑或类型?
那么现在我们来谈谈我们也许是搞错了的东西吧。我承认,“我说了我所不明白的;这些事太奇妙,是我不晓得的”。我说不清楚,专家们也未必说的清楚。把我的意见列在这里,仅供参考。文中所指 C++ 诸问题并不代表排斥,我一直用 C++ 多少年也没觉得不方便。
Objekte?Nachrichten? 之前我们谈到了面向对象的特征什么的,并且说到了一切都是可以接受消息的对象,那么到底它们是先接收消息而成为对象的,还是先成为对象而后才有能力接受消息的呢?
我们看看 Smalltalk 背后的设计原则 是怎么说的吧:
• 语言的目的:提供交流之平台。
• 对象:计算机语言当提供对象暨一通用手段,以便指代任何对象
• 消息:计算当视作对象之固有能力,借发送消息而被调用者。
• 通用代号:语言当围绕着代号设计,此代号通用于所有领域。
• 模块化:复杂系统中任何组件之实现,不得依赖于其它组件之内部细节
• 归类:语言当提供手段归类相似物;新类与固有类平等共存。
• 多态:程序指定对象之行为,非(机器)表达是也。
• 操作系统:操作系统是一坨不融于语言之物。不应有这个。
我们可以看出, Smalltalk 的面向对象机制的核心是交流,而交流的手段是消息。一切对象因为能交流,所以可以接受消息。能够接受相同消息并进行类似处理的对象具有相同的模式而聚集在一起,成为了类。这个“类”是“聚类”的“类”,不是“类型”的“类”。
因此虽然每个对象都是一个类的实例,但是它们并不束缚于类。类只是一种合并和简化。 对象和消息是同时发生的,而类紧随其后。 Ruby 中的单例方法就为我们提供了一种让对象具有个性的方法。而非常弱的类型检查又允许我们在不关注其类的情况下,让一个对象接受和处理消息。
Klassen?Typen? 类和类型是不同的。强化的类型检查很多时候是把我们的注意力引向类型上,而不是类上。我们不得不处理复杂而合问题关系较小的类型关系。比如说下面这个例子:
我们设计了一个笼子 Kaefig 类,可以接受一个操作,然后对笼子里的每个成员执行这一操作(假设是 jeder 方法)。现在我们有抽象的 Tier 类,和它的许多子类像是 Hund 等等。我们设计了一个过程 zufuehren ,它接受每个 Tier 类的对象,然后改变他们的饱食度。假设我们设计了一个动物园模拟,里面有各种各样的动物,都放在笼子里。现在我们要给每个动物喂食了!
在 Ruby里简直轻而易举:
在 C++里,就有点困难了,我们必须理清类型关系才行: a 是 Kaefig<Hund> ,可是 jeder 方法接受的是std::function<void(Hund)> ,显然 zufuehren 方法的类型是 std::function<void(Tier)> ,不能转化过去。于是只好蛋疼地把 zufuehren 用个对象包装一下,然后再用模板重新实现一遍 jeder ……这就是类型关系,但是显然,这不是类关系。
当然在 C#里我们有类型逆变。强检查和弱检查各有利弊,这是必须承认的。
但是不论如何,我们至少需要认清两个事实:
1. 类和类型是不同概念。对象和泛化的类是 OO 根本,类型不是。
2. 类的存在只是为了简便,不是束缚。连 Hello,World! 都必须用类来实现的思想从根本上是不可取的。
11.4 争论:穷途末路了吗?
丧钟为谁鸣?
前一阵子貌似还在鼓吹“垃圾语言” Java,好似非面向对象即为原始低级之别称;现在网上倒有人发出了“敲响 OO 时代的丧钟”的呼号,大有“不彻底消灭 OO 则软件不能发展”之势。即使是一个初心者,了解一下他们在争论些什么,也是大有裨益的。先就两个问题简要说明如下:
类模型可以是程序世界的基础吗?
是。 世界上一切事物都必然会和其他事物交互,因而必然具备处理他人的交互行为的能力。这种交互就是消息,能力就是消息处理函数,没有什么不妥的。世界上所有的语言都有面向对象, C 可以(用结构) , 汇编 也一样可以,而且我们每个人都在用着面向对象的成果(Unix的 FILE 系统,纯 C 加汇编写的) 。我们可以不定义类,但是对象抽象思想恒久流传。
否。 继承与泛化关系是不全面的,不足以反映世界的本质。盲目面对抽象编程,脱离具体编程的思想不可取。类 -对象只是一个实践技术而不是一个完备的计算模型,像 lambda 演算 和 图灵机 一样。 函数式编程思想(就是 Ruby 的第三范式) 和元语言抽象(第四范式) 会在不久后全面取代面向对象。
函数有类吗?
是。 Ruby 中的函数都是对象,每个函数都可以交互(绑定,调用,传递等等) ,而且这些交互是共同的,所以它们有归类。用 Proc类和Lambda类可以很好的抽象它们的行为。
否。 它混淆了源祖和衍生物的概念。函数是源祖,包含函数的对象实际是衍生物。 call 自身首先就是要定义的所谓“函数对象”。Python 和 Scala 等实际上是绑架了函数,把它们监禁在“对象”里,然后打上“call ”标签,把它们称作“方法”。当然,如果你把一个函数封装到对象里,你可以像使用一个函数那样使用对象,但这并不意味着你可以说“函数也是对象”。大多数的面向对象语言里都缺乏正确的实现一等 (first-class) 函数的机制。 Java 语言是一个极致,它完全不允许将函数当作数据来传递。你可以将全部的函数都封装进对象,然后称它们为“方法”,但就像我说的,这是绑架。缺乏一等函数是为什么 Java 里需要这么多“设计模式”的主要原因。一旦有了一等函数,你将不再需要大部分的这些设计模式。
最后在引入王垠关于这些争论的评论:
他谈到:函数式语言的程序员不要老是觉得自己掌握了真理,有什么魔法,其实没有。正如他在Cornell的教授所说,任何编程语言中都没有什么魔法或者万灵丹药,使其使用者成为好的程序员。
无论任何事情,当走向极端时都是有害的。极端化时,面向对象编程和函数式编程都试图把整个世界装入它们的特有模型中,但这个世界是在完全不依赖我们的大脑思考的情况下运转的。如果以为你有一个锤子,就把所有东西都当成钉子,这明显是不对的。只有通过认清我们的真实世界,才能摆脱信仰对我们的束缚。
不要让世界适应你的模型。让你的模型适应世界。