对象的消息模型
[ ———— 感谢 Todd 同学 投递本文,原文链接 ———— ]
目录
C++对象模型
话题从下面这段C++程序说起,你认为它可以顺利执行吗?
//C++ class A { public: void Hello(const std::string& name) { std::cout << "hello " << name; } }; int main(int argc, char** argv) { A* pa = NULL; //!! pa->Hello("world"); return 0; }
试试的确可以顺利运行输出hello world,奇怪吗?其实并不奇怪,根据C++对象模型,类的非虚方法并不会存在于对象内存布局中,实际上编译器是把Hello方法转化成了类似这样的全局函数:
void A_Hello_xxx(A * const this, const std::string& name) { std::cout << “hello “ << name; }
对象指针其实是作为第一个参数被隐式传递的,pa->Hello(“world”)实际上是调用的A_Hello_xxx(pa, “world”),而恰好A_Hello_xxx内部没有使用pa,所以这段代码得以顺利运行。
对象的消息模型
如果是研究C++对象模型,上面的讨论可以到此为止,不过这里我想从另一个层面来继续探讨这个问题。OOP的先驱人物Alan Kay在总结Smalltalk的OO特征时强调:
Smalltalk is not only NOT its syntax or the class library, it is not even about classes. I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging”.
也就是说相比类和对象的概念来讲,他认为对象交互的消息模型是OOP更为本质的特征,因为消息关注的是对象间的接口和交互,在构建大的系统的时候重要的不是对象/模块的内部状态,而是它们的交互。根据消息模型,牛.吃(草) 的语义是发送一条消息给“牛”,消息的类型是“吃”,消息的内容是“草”。如果按照严格的消息模型,那么上面那段C++代码应解释为向一个NULL对象发送Hello消息,这显然是不应该顺利执行的。类似的代码如果是在Java或C#中则会抛出空引用异常,所以Java和C#的设计更符合消息模型。
不过,Java和C#中也并非完全符合消息模型,来看一个经典的封装问题:
//C# public class Account { private int _amount; public void Transfer(Account acc, int delta) { acc._amount += delta; this._amount -= delta; } … }
上面定义了一个Account类,问题在于为什么在这个类的Transfer方法中可以直接访问另一个对象acc的私有成员_amount呢?这是不是有破坏封装的嫌疑呢?这个问题经典的答案是:并不破坏封装,封装是划分了基于类的静态的代码边界,使得类的private代码修改不影响外界,而不是对于动态对象的保护。这个解释当然是合理的,不过正如上面C++代码的解释属于C++对象模型范畴,这个解释则属于基于类的静态类型OOP语言的范畴。消息模型强调了对象内部状态的保护,只能通过消息改变其状态,而对象内部是否真的具有_amout这样一个私有成员对其他任何对象(即使同类对象)都是未知的。
如果要严格遵守消息模型实现对象内部状态的保护应该怎么做呢?我们来看一个例子,定义一个集合类,包括:1.集合对象的构造函数;2.In方法:判断元素是否存在;3.Join方法:对两个集合做交集;4.Union方法:对两个集合做并集。下面是一种Javascript实现:
//Javascript //集合类Set的构造函数 function Set() { var _elements = arguments; //In方法:判断元素e是否在集合中 this.In = function(e) { for (var i = 0; i < _elements.length; ++i) { if (_elements[i] == e) return true; } return false; }; } //Join方法:对两个集合求交集 Set.prototype.Join = function(s2) { var s1 = this; var s = new Set(); s.In = function(e) { return s1.In(e) && s2.In(e); } return s; }; //Union方法:对两个集合求并集 Set.prototype.Union = function(s2) { var s1 = this; var s = new Set(); s.In = function(e) { return s1.In(e) || s2.In(e); } return s; }; var s1 = new Set(1, 2, 3, 4, 5); var s2 = new Set(2, 3, 4, 5, 6); var s3 = new Set(3, 4, 5, 6, 7); assert(false == s1.Join(s2).Join(s3).In(2)); assert(true == s1.Join(s2).Uion(s3).In(7));
如果是在静态类型OOP语言中,要实现集合类的Join或Union,我们多半会像上面Account的例子一样直接对s2内部的_elements进行操作,而上面这段Javascript定义的Set关于对象s2的访问完全是符合消息模型的基于接口的访问。要实现消息模型Javascript的prototype机制并非必须的,真正的关键在于函数式的高级函数和闭包特性。从这个例子我们也可以体会到函数式的优点不仅在于无副作用,函数的可组合性也是函数式编程强大的原因。
Method Missing
接下来我们还要进行深度历险,让我们思考一下如果发送一条对象不能识别的消息会怎样?这种情况在C++、Java、C#等静态类型语言中会得到一个方法未定义的编译错误,如果是在Javascript中则会产生运行时异常。比如,s1.count()会产生一个运行时异常:Object #<Set> has no method ‘count’。
在静态类型语言这个问题很少受到重视,但在动态类型语言中却大有文章,来看下面的例子:
//Ruby
builder = Builder::XmlMarkup.new xml = builder.books {|b| b.book :isbn => "14134" do b.title "Revelation Space" b.author "Alastair Reynolds" end b.book :isbn => "53534" do b.title "Accelerando" b.author "Charles Stross" end }
上面这段很DSL的Ruby代码创建了这样一个XML文件对象:
<books> <book isbn="14134"> <title>Revelation Space</title> <author>Alastair Reynolds</author> </book> <book isbn="53534"> <title>Accelerando</title> <author>Charles Stross</author> </book> </books>
builder.books, b.book, b.title都是对象方法调用,由于XML的元素名是任意的,所以不可能事先定义这些方法,类似的代码如果是在Javascript中就是no method异常。那为什么上面的Ruby代码可以正确执行呢?其实只要理解了消息模型就很容易想明白,只需要定义一个通用的消息处理方法,所有未明确定义的消息都交给它来处理就行了,这就是所谓的Method Missing模式:
class Foo def method_missing(method, *args, &block) … end end
Method Missing除了对实现DSL很重要外,还可用于产生更好地调试和错误信息,把参数嵌入到方法名中等场合。目前,Ruby、Python、Groovy几种语言对Method Missing都有很好的支持,甚至在C# 4.0中也可以利用动态特性实现。
总结
本文主要介绍了对象的消息模型的特征,并比较了C++对象模型,Java、C#等基于类的静态类型语言中的对象模型与严格消息模型的差异,最后探讨了Method Missing相关话题。
参考
- Inside the C++ Object Model
- 冒号课堂 – 编程范式与OOP思想
- Alan Kays Definition Of Object Oriented
- OOP The Good Parts: Message Passing, Duck Typing, Object Composition, and not Inheritance
- Patterns of Method Missing
- Fun With Method Missing and C# 4
(全文完)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《对象的消息模型》的相关评论
DSL是动态语义的意思么?
我觉得,虽然Ruby之类的动态性增加了语言的表达方式和能力,但是限制了语言的实现的效率。类似Method Missing这样的机制,如果语言提供了支持,那么其余运算的性能一定会受到影响。
好无语~一篇文章用了4中语言。
我感觉越是自由的语言,它的应用范围就越窄,越难构建大规模的程序
@farseerfc
DSL的全称是Domain Specific Language。我觉得静态语言应该也是可以支持method missing的,而且不影响性能。不过目前静态语言的应用领域不太需要支持这种特性。不过如果要支持的话,语言可能会更加好用,再结合一些动态语言的优点来考虑,对于改进静态语言的语法,可能会有新的灵感。
不可能吧,像C++这种静态语言在编译时就能发现method missing,根本到不了运行时。。。
而且我不是很明白,引入method missing,岂不是什么调用都是合法的?那偶尔写错也太容易杯具了。
@vanxining “自由的语言”?你是在说 C 还是 Python/Ruby?或者是汇编?
@vanxining
正是编译时发现了method missing,所以可以将这种错误的函数调用改成调用特定的一个函数,这样就实现了method mising的功能。偶尔写错可以报warning之类的,或者缺省的method missing的方法是抛出一个异常,如果你重载了缺省的method missing方法就可以执行你自己定义的操作。
@yyquick
改成调用特定的函数?怎么做,让普通函数可以在编译期执行?还是引入编译期函数的概念?如果编译期函数抛出的异常,继续引入编译期异常?……以现有静态语言编译期和运行期的泾渭分明程度,要想享受DSL这种级别的动态特性,恐怕要把整个运行库在编译期再实现一遍了。C++的mpl其实何尝不是这种努力下的成果,其诘屈聱牙、曲高和寡不论,只看看它在编译期重新实现的大堆stl container就知道大致会是什么样子了——更不用说用mpl实现的dsl是什么样子。
@ex
因为静态语言没有动态指定函数名的函数调用,所以可以在编译的时候发现有没有被定义的函数被调用了,于是在生成机器执行代码的时候,就把这个调用直接指向method missing函数,其效果就是如果调用了没有被定义的函数,编译出来的代码就是调用了method missing函数。这么说应该可以理解了吧?C++是可以做DSL的,因为他继承了C的宏定义。有了宏定义,几乎就是无所不能了。只不过宏定义也有他的缺陷,一个是编译速度慢,还有使用的时候容易出错而且无论是编译时还是运行时的错误都比较难排查,再有就是写法太自由,可读性不好,令人难以理解。
@yyquick
看看Ruby的def method_missing(method, *args, &block)的定义就知道,静态类型语言的参数和返回值类型不好处理。如果是C++那就只能强制类型转换了,进来强转,出去也强转,这就失去DSL的特点了。
【深度历险】让我想起了最初的介绍delphi技术的tw网站。。。。。。。。。
哈哈,我所指的“自由”主要是什么都不用定义,直接用,例如变量什么的。感觉都是在写一些随用随抛的玩具。当然,这只是我个人的感觉,我还是挺佩服写出巨型JS程序的牛人的,他们用的技术不比C/C++/Java这些语言差。
@vanxining
c也是非常自由的。但这个星球上最复杂的程序都是由他构建。
@Todd 如果我的理解没错,对象的消息模型 指的是对象的接口?
如果是这样,确实可以说道说道
c++本来就不是写给不尊崇规约写代码的程序员们。她是建立在充分信任程序员的素养的基础上的。至于她怎么实现oop中的消息对象机制,这是编译器的问题。
@Todd
类型强转其实没什么大不了的。如果支持RTTI的话,估计也不用强转。输入参数可以从堆栈里面找到,总是有办法处理的。我的意思是,静态语言其实也可以搞一下method missing之类的支持。但是其意义到底有多大,还需要进一步的研究。
c++不是人用的 只有神一般的程序员才能驾驭它
@vanxining
这可不一定~。Ruby和Python构建的大规模程序难道还少吗?
用动态语言来实现DSL是很方便的。
拿Ruby来举例,有一个CStruct的gem (http://cstruct.rubyforge.org/),就是在Ruby中来描述C语言结构体的,可以说是个小型的DSL.
比如说一个Win32的结构体:
typedef struct _OSVERSIONINFOEXA {
DWORD dwOSVersionInfoSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformId;
CHAR szCSDVersion[ 128 ];
WORD wServicePackMajor;
WORD wReserved[2];
} OSVERSIONINFOEXA;
用CStruct来描述就是这样:
class OSVERSIONINFOEXA < Win32Struct
DWORD :dwOSVersionInfoSize
DWORD :dwMajorVersion
DWORD :dwMinorVersion
DWORD :dwBuildNumber
DWORD :dwPlatformId
CHAR :szCSDVersion,[ 128 ]
WORD :wServicePackMajor
WORD :wServicePackMinor
WORD :wReserved,[2]
end
这样的DSL给Ruby及其他动态语言带来了可以直接存储C结构体的能力,也能更方便的调用OS API。
#include
#include
class A {
public:
void Hello(const std::string& name) {
std::cout << "hello " <Hello(“world”);
return 0;
}
在vc6.0中报异常:
error C2679: binary ‘<<' : no operator defined which takes a right-hand operand of type 'const class std::basic_string<char,struct std::char_traits,class std::allocator >’ (o
@klvoek
void 函数返回0?
然后 <Hello("world") 又是啥用法?
没有截止的递归加字符串与void比较? 还是说是某中神奇的方法.
还有name参数没有用上吧.
最近在看Objective-C,发现C语言也可以演化出如此优美的结构,ObjC和C++都是从SmallTalk演化或者借鉴而来的,却走了不同的路线:一个是消息机制,一个是对象封装。我一直一来都不喜欢C++,我是觉得它在2003年左右的那些年里被很多不写程序的人神话了,“对象”被当做神器一般的东西。我只能说,C++是被人误导了,至少学C++的人被很多所谓的C++专家给忽悠了,同时反过来影响了C++的发展。
相对于C++,Objective-C谦虚了很多,秉承C的简洁,拓展并巩固了设计者的初衷,并且以SmallTalk设计者之父所说的,“对象”是个衍生概念,“消息”才是本质!
@yyquick
实际上就是一种操作符重载,和 C++ 现支持的其他类型操作符重载一样。重载方法调用操作符。
Really good blog. Thank you for writing.