huaxiazhihuo

 

消息发送杂谈

      最近在看MFC的代码,虽然这破玩意,老朽已经很熟悉了得不能再熟悉了,但是这些破代码自由其独有的吸引力,不说别的,单单理解起来就非常容易,比之什么boost代码容易看多了,单步调试什么的,都非常方便,堆栈上一查看,层次调用一目了然。一次又一次地虐这些曾经虐过老朽的代码,也是人生快事一件。平心而论,mfc的代码还是写得挺不错的,中规中矩,再加上过去九十年代之初,16位的windows系统,那个时候的面向对象的c++的风靡一时,完全采用标准c++,能做成这样,实属难能可贵,也在情理之内。并且,后面压上com之后,mfc也不崩盘,采用内嵌类实现的com的方式,也很有意思。然后,从mfc中也能学到不少windows gui的使用方式还有各种其他杂七杂八东西,虽然win32已经没落。但是里面的技术还是挺吸引人,可以消遣也不错。当然,对于新人,mfc不建议再碰了,mfc真是没饭吃的意思。你想想,一个gui框架,没有template可用的情况下,而逆天c++11的lambda作为匿名functor,更加不必提了,只有虚函数和继承可用,也没有exception,能搞成mfc这副摸样,的而且确是精品。其实,后来的巨硬也有救赎,看看人家用template做出来的专为com打造的atl又是什么样子呢,然后建构在atl的windows thunk基础上开发的wtl又是怎样的小巧玲珑。巨硬在template上的使用还是很厉害的,atl将template和多继承用的真是漂亮。人家几十年前就将template和多继承用得如此出神入化,反观国内,一大批C with class又或者狗粉一再叫嚣template滚出c++,多继承太复杂了,运算符重载不透明,心智负担,隐式类型转换问题太多,virtual是罪恶之源万恶之首,构造函数析构函数背着马猿做了太多事情,exception对代码冲击太大,打断代码正常流行,时时刻刻都好像隐藏着不定时炸弹。依本座看来,C++中一大批能够显著减少重复代码,带来类型安全的拔高抽象层次的好东西,对于这些C语言卫道士而言,都是混乱之物。其实,c语言就是一块废柴,抽象层次太低,可以做文章的地方太少了。
      就以构造函数和类型转换operator为例,来看看怎么用于C的char *itoa(int value,  char *str,  int radix)。
      itoa的参数之所以还需要str入参,那是因为C语言中缺乏返回数组的语言元素,所以调用者要提供一个字符数组作为缓冲用于存放结果,但是这个多出来str参数真是没必要啊,因为语言能力的欠缺,所以只好把这个负担压到猿猴身上。也有些itoa的实现没有这个累赘str的入参,而是内部static的字符数组,用于存放结果并返回给上层。这个版本就只有两个入参了,但是也太不安全了,别提多线程了。假如,有一个函数fn(char* str1, char* str2),然后,这样调用fn(itoa(num1),itoa(num2)),画面太美了。另外,那个有多余str参数版本的itoa也好不到哪里去,要劳心费神准备两块字符数组,然后还要保证参数传递的时候不要一样。反正C语言的粉丝整天很喜欢写这些重复代码,并且美其名曰掌控一切细节的快感。
请看构造函数和类型转换operator怎么解决。太easy了。

struct ToString
{
    
char text[28];
    
int length;

    ToString(
int n)
    {
        
//转换字符串,结果存放于text中
    }

    
operator const char*()
    {
        
return text;
    }
};
      并且,这里的ToString还可以安全的用之于printf里面呢,因为它本身就是字符串的化身。为什么是ToString,因为它不仅仅要它ToString int,还有double,bool,char,……
      不好意思,扯远了,只是想说,框架或者函数库的表现能力也要取决于语言本身的表达能力。就好像C#就可以做出linq那样爽快的框架,java再怎么拼命也捣鼓不出来一个一半好用的linq,C++也不行,但是C++可以捣鼓类似于haskell中的map,filter,fold等,  并结合linq的后缀表达方式。就好比下面这样
      vector<int> nums = {...}
      Range(nums).Map(_1 * _1).Filter(_1 % 2).CopyTo(dest); // 用了boost中的lambda表达法,因为的确很简洁,没法控制。对于复杂情况,当然要用C++11原生的lambda
      勉勉强强差可满足吧。如果C++的lambda参数可以自动推导就好了,不过也没什么,主要是ide下用得爽。用泛型lambda也能将就。
      所以,回过头来,再看看mfc(没饭吃),就可以了解其各种隐痛了。真的,以90年代的眼光来看,mfc真是做到极致了。mfc不可能走win32下窗口函数C语言那样的消息发送消息反应的正路(邪路)吧。窗口函数这一套,在90年代面向对象盛行的时代,绝对不能被忍受,只是到了前几年,才被发现其价值原来不菲,这是解耦合砍继承树的好手法,老朽在前几年也跟风吹捧窗口函数的那一套。平心而论,smalltalk的这一套消息发送的动态语言,确实是很强有力的抽象手段,我不管目标对象能否反应该消息,闭着眼睛就可以给你发送消息,你能反应就反应,不能反应就拉倒,或者调用缺省的反应方式,就好像DefWindowProc(职责链模式?),又或者是抛出异常,怎么做都可以。一下子就解开了调用者和目标对象的类型耦合关系。面向对象中,消息发送和消息反应才是核心,什么封装继承多态,那是另一套抽象方式,虽然坊间说这也是面向对象的基本要素,但是不是,当然,这或许也只是个人观点。
      或许,从某种意义上讲,C++/java/C#一类的成员函数调用形式,其实也算消息发送吧。比如,str.length(),就是给对象str发送length的消息,然后str收到length消息,作出反应,执行操作,返回里面字符串的长度。靠,这明明就是直接的函数调用,搞什么消息发送的说辞来强辩,颠倒是非黑白,指鹿为马。可不是吗?编译器知道str是字符串类型,知道length成员函数,马上就生成了高效的函数调用方式。在这里,没有任何动态多态的可能,发生就发生了,一经调用,动作立马就行动,没有任何商量的余地。耦合,这里出现强耦合,调用者和str绑在一块了,假如以后出现更高效率更有弹性的string的代替品了,可是没法用在这里了,因为这里str.length()的绑定很紧很紧。
      人家消息发送就不一样了,动态的,可以动态替换对象,替换方法,弹性足足。并且,消息发送的模式下,对象收到消息,要判断消息,解析消息,找到消息的执行函数,最后才终于执行任务。这么多间接层,每一层都可以做很多很多文章。比如,在消息到达对象之前做文章,就可以搞消息队列,把消息和参数暂存起来,这个时候,什么actor模式就大放异彩,至于undo,redo,更加是小菜一碟。然后呢,再给对象安装消息解析器,把消息和消息参数转换成其他类型消息。比如原本对象不能反应这条消息,但是对消息参数稍加修饰,然后在发送给对象,这不就是适配器模式。总之,可操作可挖掘的空间太大了,远远不止23条。
      但是,封装继承多态就一无是处了吗?不是的,最起码一点,编译期间可以报错。因为的确有很多时候,我们明明就知道对象的类型,明明就知道对象不可能是其他类型,比如字符串,比如复数,比如数组这些玩意,无论如何,它们都不需要动态消息的能力。我们就知道手上的对象就是字符串就是复数,不可能是别的,并且,我们就是要明确地调用length函数。我们就是要编译器帮忙检查这里潜在的语法类型错误,比如对复数对象调用length函数,编译器马上就不高兴了。并且,一切都是确定的,所以编译器也能生成高效的代码,高效的不能再高效了。对此,消息发送的面向对象就做不到了,不管是什么对象,int,string,complex种种,都来个消息发送。这样一来,静态类型检查和高效的代码,就木有了。
考察一下,面向对象有等级之分,一步一步,有进化的阶梯。每进化一次,就多了一层间接,类型耦合就降低,就进一步超越编译器的限制,当然,也意味着编译器帮忙检查类型错误生成高效代码就弱了一分。事情往往就是,有所得必有所失。少即是多,多即是少。因此,可推得少即是少,多即是多。少始终是少,多始终是多。
      一切,还是要从C语言说起,C语言中,没有class,没有函数重载。函数名是什么,最后就是什么。在这种条件下,代码多了,每个新的函数名字要考究半天,一不小心,要么函数名字就会很长,要么函数名字短了要冲突或者不好理解。但是好处是,最后生成目标代码时,什么函数名字就是什么名字,所见即所得,没有异常,不会捣鬼,于是其他各种语言都可以高高兴兴开开心心调用。猿猴观码,也很清晰。C++也是在这里赚了第一桶金。其实,这么苛刻的条件下,最考究猿猴的代码架构能力,架构稍微不好,最后都势必提早崩掉,前期就可以过滤很多垃圾架构。
      然后就是C with class了,开始在函数名字上面做文章了。同一个函数名字依对象类型,开始拥有静态多态能力了。比如,str.length(),这是对字符串求长度。数组的变量,nums.length(),对数组求长度。同一个length的名字,用在不同的对象上,就有不同的意义。这如何做到呢,最初,cfront(第一版C++编译器)的处理方式是,可以说是语法糖,就是在名字和调用形式上做文章,比如,str.length(),变成,string_length(&str),array_length(&nums)。别小看这点小把戏语法糖,这真是有力的抽象手法。不说别的,就说起名字吧,可以统一length了,无须费思量string_length,list_length了。然后,对象统一调用方式,str.length(),list.length(),函数确定这种吃力不讨好的事情就交给编译器去做好啦,解放部分脑细胞。这,的确很好,但是,全局函数是开放式的,而对象的成员函数是封闭的,一旦class定义完毕,成员函数的数量也就定死了。猿猴最讲究东西的可扩展性,不管成员函数多么方便多么抽象有力,就扩展性而言,就差了一大截,其他优势都弥补不了。语义上看,扩展成员函数的语法完全与原生的一样,增加一个简单的语法形式来扩充,但是多年下来,标准委员会都不务正业,哎。显然,编译器的类型检查能力和生成的代码性能,没有任何减少,但是,猿猴看代码,不能再所见所得了,必须根据对象类型,才能确定最终的目标函数。就这么点小改进,当时C++马上就展示其惊人的吸引力。假如,C++只能留在这一层,相信到今天为止,可以吸引到更多的c粉。可是,C++开始叛变。
      C++的函数重载,还有操作符重载,外加隐式类型转换和隐式构造函数,还有const,volatile修饰,当然,代码可以写得更加简洁,编译器可以做的事情也更多啦,但是函数的调用再也不明确了。部分专注于底层的猿猴的弱小的抽象能力把控不住了,不少人在这里玩不动了。此外,命名修饰把最终函数名字搞得乱七八糟,二进制的通用性也要开始废了。导致C++的dll不能像C那样到处通吃。像是狗语言就禁止函数重载这个功能。大家好像很非难C++的操作符重载,但是haskell还能自定义新的操作符呢。虽然在这里,编译器还能生成高效代码,但是,各种奇奇怪怪类型转换的规则,编译器也偶尔表现出奇,甚至匪夷所思,虽然一切都在情理之内。
      其实,不考虑什么动态能力,单单是这里的静多态,基于对象(俗称ADT)的抽象模式,就可以应付70%以上的代码了。想想以前没有静多态的C日子是怎么过的。
      此时,开始兵分两路,C++一方面是动多态发展,表现为继承,多继承,虚继承,虚函数,纯虚函数,rtti(废物,半残品),到此为止了,止步不前;另一方面是继续加强静多态,王者,template,一直在加强,模板偏特化,template template,varidiac tempalte,consexpr, auto,concept,……,背负着各种指责在前进,就是在前进。C++企图以静态能力的强悍变态恐怖,不惜榨干静态上的一点点可为空间,累死编译器,罔顾边际效应的越来越少,企图弥补其动态上的种种不足。这也是可行的,毕竟haskell都可以做到。template的话题太庞大了,我们言归正传,面向对象。
      下面就是被指责得太多的C++多继承,虚函数,RTTI,脆弱的虚函数表,等,这些说法,也都很有道理,确是实情,兼之C++没有反射,没有垃圾回收,用上面这些破玩意捣鼓,硬着头皮做设计做框架,本来就先天能力严重不足,还要考虑内存管理这个大敌(循环引用可不是吹的),更有exception在旁虎视眈眈,随时给予致命一击。更要命的是,多继承,虚函数,虚继承,这些本来就杀敌八百自伤一千,严重扰乱class的内存布局,你知道vector里面随随便便插入元素,对于非pod的元素,不仅仅是移动内存,腾出新位置来给新对象安营扎寨,还要一次又一次地对被移动的对象执行析构拷贝构造。没有这些奇奇怪怪的内存布局,vector的实现应该会清爽很多。稍微想想,这实在太考究猿猴的设计能力,其难度不亚于没有任何多态特性的C语言了。可以这么说,继承树一旦出现虚继承这个怪胎,整体架构就有大问题,毫无疑问,iostream也不例外。不过,如果没有那么多的动态要求,好比gui框架的变态需求,严格以接口作为耦合对象,辅以function,也即是委托,又可以应付多起码15%的局面。其实,必须要用到virtual函数的时候,将virtual函数hi起来,那种感觉非常清爽,很多人谈virtual色变,大可不必。C#和java还加上垃圾回收和反射,这个比例可以放大很多。在这种层次下,接口最大的问题是,就好像成员函数,是封闭的。一个class定义完毕,其能支持的interface的数量也就定死了,不能再有任何修改。interface可以说是一个class的对外的开放功能,现实世界中,一种东西的对外功能并不是一开始就定死了的,其功能也在后来慢慢挖掘。但是,C++/java/C#的接口就不是这样,class定义完毕,就没有任何潜力可言了。明明看到某些class的能力可以实现某些接口,甚至函数签名都一样,对不起,谁让你当初不实现这个接口。对此,各种动态语言闪亮登场,或mixing或鸭子类型。接口还有另一尴尬之处,比如,鸟实现了会飞的接口,鸭子企鹅也继承了鸟,自然也就继承了会飞的接口,没办法不继承。面对着一个需要IFlyable参数的函数,我们顺利的传一只企鹅进去,然后企鹅再里面始终飞不起来,就算企鹅在被要求飞的时候,抛出异常,也不过自欺欺人。这种悲剧,就好像有些人很会装逼,最后一定会坏事。搞出接口这种破事,就是为了让编译器做类型检查的。又有人说,bird应当分为两类,会飞的和不会飞的,这的确能解决飞行的尴尬。但是,有很多鸟具备捉虫虫的能力,然后又有那么一小撮鸟不会捉虫只会捉鱼,难道又要依据捉虫能力再划分出鸟类。于是鸟类的继承树越长越高,画面越来越美。这分明就是语言能力的不足,把问题交给猿猴了。请谨记,接口就是一个强有力的契约,既然实现了一个接口,就说明有能力做好相关的事情。再说,既然interface这么重要,于是我们再设计class的时候,就自然而然把精力放在interface这个对外交流媒介的手段之上了,而忽视了class本身的推敲。class最重要的事情就是全心全意做好独立完整最小化的事情,其他什么对外交互不要理会。一个class如果能够完整的封装一个清晰的概念,后面不管怎么重构,都可以保留下来。但是,interface会分散这种设计。接口的悲剧就在于企图顶多以90分的能力去干一百分的事情,并且还以为自己可以做得好,硬上强干,罔顾自身的极限。往往做了90%的工作量,事情恰恰就坏在剩下来的10%上。
      于是,狗语言走上另一条邪路,鸭子类型。只要class,不,是struct,这种独特关键字的品味,只要某个struct能够完全实现某个interface的所有函数,就默认其实现了这个接口。并且,狗语言还禁止了继承,代之以“组合”这个高大上的名词了,但是,细究一下语义和内存布局(忽略虚函数表指针),你妈的,不就是一个没有virtual继承的弱多继承吗?显式的继承消失了,隐式的继承还存在的,好了,还不让你画出继承树关系图,高高兴兴对外宣称没有继承了,没有继承并不表示继承的问题木有存在。但是,因为狗语言的成员函数方法可以定义在class,不,struct外面,其扩展性就非常好了,对于一个interface,有哪些方法,本struct不存在,就地给它定义出来,然后,struct就轻松的实现了该接口,即使原来的struct不支持该接口,以后也有办法让它支持,很好很强大。之所以能做到这一点,那是因为狗语言的虚函数表是动态生成的。小心的使用接口各种名字,部分人应该狗语言用起来会相当愉快。可是,你妈,不同接口的函数名字不能一样啊,或者说,同一个函数的名字不能出现在不同接口中。不过,这个问题并不难,不就是不一样的名字吗,c语言中此等大风大浪猿猴谁没有见识过。对于狗语言,不想做太多评断,只是,其扩展性确实不错,非侵入式的成员函数和非侵入式的接口,理应能更好地应付接口实现这种多态方式,只是,编译器在上面所做的类型约束想必会不如后者,重构什么的,想必不会很方便。自由上去了,约束自然也下来了。听起来挺美,但是内里也有些地方要推敲,反正老朽不喜欢,以后也不大会用上,当然,给money自然会用,给money不搞c++都没问题。老朽还是比较喜欢虚函数的接口,更何况c++通过奇技淫巧也能非侵入式的给class添加接口。在静态语言中搞这种鸭子类型的动态语言接口,显得有点不伦不类。
      然后就是com接口的面向对象,完全舍去编译器对接口类型的约束,自然能换来更大的自由。由于com的语言通用性目标,所以搞得有点复杂,但是com背后的理念也挺纯洁。老朽猜测com好似是要在静态语言上搭建出一个类似于动态语言的运行平台,外加语言通用性。其契约很明确,操作对象前时,必须先查询到对象支持的接口,进而调用接口的函数。这里有意思的地方在于面对着一个com对象,你居然没有办法知道到它究竟实现了多少接口。
      最后就是消息发送了,其能力之强大,谁用谁知道。原则上讲,可以看成对象拥有的虚函数表的方法无穷多,又可以把每一条消息看成一个接口,那么,对象可能就实现了无穷多的接口。你说,面对着这样对象,还有什么做不出来呢。真用上消息发送这种隐藏无数间接层,就没有什么软件问题解决不了的。任何软件问题不就是通过引入间接层来解决的嘛。现在用上消息发送这种怪物,就问你怕不怕。没有免费午餐,自然要付出类型安全的危险和性能上的损失。

posted on 2016-05-10 22:40 华夏之火 阅读(1633) 评论(3)  编辑 收藏 引用 所属分类: 编程语言杂谈

评论

# re: 消息发送杂谈 2016-05-11 12:18 Richard Wei

这里有意思的地方在于面对着一个com对象,你居然没有办法知道到它究竟实现了多少接口。微软自己也也意识到了这个问题, 于是WinRT里就有了IInspectable::GetIids, https://msdn.microsoft.com/en-us/library/br205822(v=vs.85).aspx  回复  更多评论   

# re: 消息发送杂谈 2016-05-13 14:49 天下

好文,赞。  回复  更多评论   

# re: 消息发送杂谈[未登录] 2016-07-07 09:27 x

mark。精品文章,收藏之  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(6)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜