Mathematica 面向对象编程解决方案


很多朋友都在问如何让Mathematica能够像C++一样进行面向对象编程,我在暑假阅读《计算机程序的构造与解释》时突发灵感,信手写了这么一个Mathematica面向对象包,特把相关技术细节整理下来。

面向对象原理

什么是面向对象?维基百科说,“面向对象程序设计英语Object-oriented programming,缩写:OOP),指一种程序设计范型,同时也是一种程序开发的方法。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性”。

C语言是面向过程的,我们把要做的事情分成一个个函数来完成,比如我要写一个公交车自动收费系统,那么我需要为公交车行驶,公交车报站,公交车开关门,乘客上车,乘客交费,乘客刷卡等行为编写相应的函数。

后来人们发现,程序里的很多函数有很强的分类性与隔离性,公交车行驶可能和乘客上车完全不相干;而且人类认知世界的方式也是先知道有某样东西,然后知道它有什么用。因此,一种面向对象的程序设计思想逐渐诞生。


面向对象里最核心的概念就是对象。拿生物作类比,类就是一种物种,对象就是一个特定的生物;类是抽象的概念,对象是存在的实体。因此,对于“猫”这个类,它有性别,毛色,年龄等数据属性(称为成员变量),还有捉老鼠,睡觉等行为属性(称为成员函数);而一只特定的猫,就会拥有它自己特定的性别,毛色和年龄,但对于成员函数,相同类的对象是相同的,只是函数执行时会用到对象的成员变量值而计算出不同的结果


类概念的引入,使得我们可以把相关性很强的函数与数据进行封装,增强程序的模块化程度;同时面向对象更加适宜我们对一个大型项目进行建模

那么,C++里的面向对象是什么样子的呢?我们来看个例子

假设我们有两个学生对象 tom和kate,他们都有speak函数来说出自己的名字,

  1. tom.speak();
  2. kate.speak();


运行结果可能是

  1. My name is Tom.
  2. My name is Kate.





我们看到,两个speak函数具有相同的签名(signature,表示这个函数的参数类型,数目,返回值类型等一系列特征),但执行时带入了不同的参数。实际上,C++的成员函数与普通函数有个本质区别,即成员函数有一个隐含参数this,表示调用该成员函数的对象的指针,这样,成员函数就能够通过this访问这个对象的成员变量与函数,从而根据不同的对象算出不同的结果。这就是C++面向对象技术中最关键的地方,this指针的引入,实际上,在其他面向对象语言中都有相似的机制。


Mathematica 面向对象的实现


那么,我们该如何在Mathematica模拟出面向对象的效果呢?


首先看类的结构是什么样子的。


类,其实就是一组成员变量和成员函数的列表。假设我们有一个 man 类,包含 name 变量和 say 函数。那么,我们可以把man定义成如下的形式

  1. {{“name”, Null},{"say",Null}}



我们无需区分变量和函数(它们统称为),直接把它们放在同一个List里。每一个域都对应一个二元List,分别表示它的名字和值;这么做是为了方便我们访问这个域。

这里需要额外插点其他技术知识。面向对象的类里有一种特殊的函数与变量,称为静态函数与静态变量。这些变量是不属于任何对象而是属于这个类的,并且被所有对象所共享。比如“猫”类,我们有一个静态变量maxAge,表示猫的最大寿命,比如我们取30,那么所有的猫对象都可以访问这个数据甚至修改它,但这个数据不属于任何猫对象,而是猫类的一个公共属性。

我们的Mathematica实现里,没有明显的区分静态函数、静态变量;事实上,我们的对象具有和类一模一样的结构,通过类访问域,得到的就是静态域,通过对象访问,得到的就是成员域。我们的这种实现有如下的局限:对象中也保存着静态域的数据;对象可以修改静态域但是不会更新类的静态域(这个缺陷可以通过小修改来克服,暂时没有实现)


正如上面所说,我们的对象具有和类完全相同的结构,因此从一个类构建一个对象只需要复制这个类的结构即可。


接下来的一个问题便是,我们该如何访问这个类的域呢?

我们在前面的擂台帖子(http://forum.simwe.com/thread-997834-1-1.html)中给出了如下的访问方法:

  1. tom = New[man];
  2. tom["year"] = 1989;
  3. tom["name"] = "tom";
  4. tom["say"][]
  5. tom["age"][]




相当于用一个下标作为索引访问具体的值。FlyingDuckman 通过定义New来实现,我当时已经指出其中的问题;而且还有一个更深入的问题在于,如此实现,New将不堪重负,每定义一个类就要定义这么一组New函数,完全不符合我们的需求。

对于前一个问题,我们就要仿造C++来引入this指针,这个会在后面介绍;后一个问题,我们要使用一种特殊的机制,称为 Dynamic Dispatch(参见 http://en.wikipedia.org/wiki/Dynamic_dispatch)。

不知道大家是否还记得或者阅读过这篇文章 http://forum.simwe.com/thread-991845-1-1.html,关于Mathematica中的闭包实现。我们这里不需要使用闭包,但是它给我们提供了一种无与伦比的技术:匿名变量


匿名变量?Ok,这是我自己杜撰的名词,但它名副其实。那个例子里,你知道y是什么么?不,你永远不知道,除非你用?去搜索所有以y开头的变量;但是y却是客观存在的,并且每当你调用Add函数时,不仅改变了y的值而且返回它。这就是匿名变量的全部内涵。

那么匿名变量对我们有什么帮助呢?它就是我们实现Dynamic Dispatch的核心。


我来举一个简单的例子。

  1. lily = Module[{me},
  2.   me["name"] = "Lily";
  3.   me["say"] := Function[{}, Print["My name is ", me["name"]]];
  4.   me
  5.   ]
  6. lily["say"][]





OK,把这段代码仔细品味品味,我的面向对象思想已经昭然若揭。
我不想过多解释这段代码,只希望你好好考虑如下的问题:lily是什么?lily[“say”][] 是怎么执行的?


这个解决方案的诞生不是灵光一现,它足足折磨了我三天三夜;感谢《计算机程序的构造与解释》,这个解决方案几乎完全是由它给出的。


this指针的实现


那么this指针呢?你肯定立马可以看出来,上面的me就是this指针。对于类的域函数定义,包含两种类型:第一种是静态函数,定义成 Function[{x},y],y仅仅是x的函数,与对象无关;第二种是成员函数,定义成 Function[{this},Function[{x},y]],y是this和x的函数,this代表传入的对象。在对象中dispatch函数时,根据嵌套的Function层数决定是否是静态函数。至此,我们的面向对象体系基本架构完成。


以下是使用我编写的程序包实现的小程序。


  1. << DabaoClass`
  2. man = DefClass["man", {year, name}]
  3. AddField[man, "age"]
  4. man["age"] = 
  5.   Function[{this}, Function[{}, First[DateList[]] - this["year"] + 1]];
  6. AddField[man, "say"]
  7. man["say"] = 
  8.   Function[{this}, Print["My name is ", this["name"], "\nI'm ",
  9.      this["age"][], " year's old"] &];
  10. tom = New[man];
  11. tom["year"] = 1989;
  12. tom["name"] = "tom";
  13. tom["say"][]
  14. tom["age"][]


运行结果

  1. My name is tom
  2. I'm 23 year's old

程序包下载 DabaoClass.rar

posted on 2011-08-25 19:18  大宝pku  阅读(3311)  评论(0编辑  收藏  举报

导航