RSS 2.0 Feed

Tuesday, July 12, 2005

一直以来对于什么是AOP没有太深入的概念,直到读完白话面向智能体编程(Agent Oriented Programmig, AOP)[1]之后仍然是没有很深入的认识。也许日后有机会可以深入了解一下,不过今天想将读完该Blog之后的想法记载于此,尽管这样的思考也许挺幼稚的。我对于这些概念没有太多的知识,如果认识有误欢迎指正(通过交流获得知识我觉得是比较有效率的)。

  首先说明一下,这里的AOP当中的A是Agent,而不是Aspect。虽然目前我没有摸清楚Agent和Aspect的具体差别在哪里,但是可以肯定是有区别的。我想Aspect-OP应该是强调概念与实现的一对一映射,其中单一概念与单一职责原则比较接近,单一实现我暂时找不到比较接近的概念。关于单一实现可以这么说:我们大家都知道MVC的模式、或者一些其它的模式,这些模式其实通过我们口中说出来只是一种想法,或者说是概念。而最终形成代码的才是实现,可是也许会有很多地方会用到MVC,于是对于MVC就会有各种各样的实现。(参见“白话面向智能体编程(Agent Oriented Programmig, AOP)”系列第一篇一开始引用的文章The Ted Neward Challenge (AOP without the buzzwords)[2],原文的链接无效,这个是google出来的。)引用[2]的作者在文中提到他的研究当中最夸张的情形是,在某个软件当中有一种设计上的概念竟然有10万个(次)实施。(In one system I've studied, the concept:implementation ratio for that single requirement is well in excess of 1:100,000. )也许这么说还比较难理解,那么我们说说记录异常(to log failures),可能我们会在软件当中随手写道:
MessageBox.Show(ex);
又或者
logfile.WriteLine(ex.Message);
还可能是
myLogger.LogObjectTypeA(ex, objectA);
myLogger.LogObjectTypeB(ex, objectB);
...

  而事实上将错误记录下来应该是一个问题,或者一种设计上的概念,然而却会在软件当中存在多种不同的实施方案。这种情况对于维护来说可能是一件相当麻烦的事情,因为本来都是同一个概念里面的东西,如果我们因为概念上的问题需要进行更新维护,可能就需要对所有不同的实现进行维护,这将会是一件相当烦人的事情。

  上面说的是我对Aspect-OP的理解,那么Agent-OP呢?我想“白话面向智能体编程(Agent Oriented Programmig, AOP)”的系列说的还是比较容易理解的,其大意是,我们需要一个跟真实世界更加接近的模型。在Agent-OP里面有很多与OOP不一样的思考方向,例如“有心智”或者说“有目标感”,我想这一点是比较重要的概念。说到OOP我们自然能够想到“对象”,对象的其中一个作用是划定知识界限——什么属于这个对象以内的(比如对象当中的成员),什么属于这个对象以外的。这一概念对于软件界是一个里程碑,应当说是非常有意义的。然而我们也应该能够体会到OOP本身并没有对“驱动力”进行探讨,而事实上目前的软件中的“目标感”更多地是通过下列两种力量驱动的:用户,以及软件背后的设计。

  用户驱动并没有什么问题,毕竟使用软件的是人。然而通过软件背后的设计来驱动“目标感”,我一直都觉得不是很顺畅——因为这种设计相对来说是静态的,基本上不会根据环境的变化进行相对的调整。比如引用[1]当中提到的一个“鸡块熟了之后捞起来”的例子,也许我们会设计一个ChickenClass和一个ChefClass,ChickenClass有一个事件Cooked告诉ChefObject某个CheckenObject熟了,然后我们就会有下面的代码(C# 2k5):

CheckenObject.Cooked += ChefObject.Checken_Cooked;

  这个设计的后果是,不管厨师有多忙,鸡块觉得自己熟了之后就会踢厨师一脚,这时候厨师不管情愿不情愿都得把鸡块捞起来。事实上目前的绝大多数软件中的“厨师”本身根本就没有情愿不情愿想法,只不过是一个奴隶、一个工具而已。有人会说,有的软件中的“厨师”是有心智的,但我想还是与Agent-OP是有区别的,其区别可能在于两个方面:

1、有可能这只是无意识造成的偶然结果;
2、如果是有意识设计出这样的“厨师”,很有可能并非用的Agent-OP的工具。(可能制作的方法并不规范,可能存在一些问题。)

  也许Agent-OP比较前沿,我自己也说不清楚,换一个面向过程和面向对象来说吧。首先让我们时光倒流20年,假设我们正在使用C语言。我们能够将某些知识限定到某个范围以内吗?我想可以用struct,我们可以设计一个v-table,就像C++背后做的那样。于是我们也一样可以写出类似obj->Member(obj2); 这样的东西来,例如:(obj.v_table[ID_Member])(obj2);

  可这仍然不是OOP,还有很多的问题是需要有专门的OOP工具才能够解决的。Agent-OP也一样,我们目前如果要制作一个Agent-OP的软件,工具还是相当缺乏的,甚至连人的思想也相当缺乏的。不要忘了,光有OOP的工具,如果使用的人还是以面向过程的方式去思考和设计,最终出来的东西也不是OO的。我想Agent-OP也一样,例如让我来使用Agent-OP的工具兴许就弄不出什么花样来。

  我为什么会觉得需要Agent-OP呢?因为Agent-OP应该能够提供一定的自适应能力(比如什么时候鸡块算是熟了,什么时候我有时间去处理,以什么样的方式去处理等),有了自适应性才有可能提供自组织能力。没有自组织能力的系统,其整个设计实际上仍然是硬梆梆的。关于这点我们可以用制造行业的发展来做对比:现在的制造业有一种较新的概念叫做“柔性制造”,与此相对的是“刚性制造”。对于“刚性制造”来说,生产线的各个机构的运动方向只能够或者前后或者左右,顶多可以换一个模子或者刀具,因此其任务和目标是定死的。当任务或者目标发生改变的时候,比如原来是造汽车门的,现在要制作飞机机舱门了,那么整个生产线就报废了,对于软件来讲就是需要重新设计。但“柔性制造”就不一样了,生产线上的工作机构有许多的自由度,要制作什么完全依靠工控程序的控制,因此面对任务和目标的变化,只需要更换工控程序就可以了。对于软件来说,我们也希望能够仅仅替换某一个很小的“智能部件”,就能够在一定范围内改变任务和目标,因为我们都知道软件的需求(也就是任务和目标)的改变往往是代价非常高的。要达到这一目的,“智能部件”必须能够自行适应其外部的系统状态,或者说能够与系统的其它部分自行组织起来。要做到所有这些,以目前的技术条件看起来是相当困难的。

  说到这里,也许已经看到我对于Agent-OP的理解跟引文[1]中所述的有一定的出入了。正是因为这些出入,我对于原文的某些观点有一些不同意之处,且容我细细说来:

痒处一:OO并没有对现实世界中的实体加以区分
  引文[1]的作者认为需要区分“发票”和“员工”,因为“发票”是没有智能的死物,而“员工”是有智能的活物。死物只是被动的接受各种的操作,而活物除了会主动的进行操作之外,还有可能主动的进行调整,甚至是拒绝操作。
  然而我认为现实世界如此,并不代表虚拟的世界也必须如此。难道虚拟世界就不能够如同动画片或者科幻片中的想象那样,存在具有思想感情的“发票”吗?更尖锐一点说“机器(人)”是死物呢,还是活物?反过来也一样,“员工”一定是活物吗?还是那个例子,如果你家里有一个机器人员工你打算怎么办?当然,这样的例子比较变态,但是我想从中说明,虚拟世界的对象是否应该具有心智,并不取决于现实世界中影射的对象是否具有心智。而对于作者说的“可以引入计时器或者多线程,但是总是因为与现实不符而不爽”是非常认同的,然而实际上引起这一问题的实质并非在于虚拟对象与现实对象在心智上的映射不能相对应。关于这一问题,我想在后面的条目中会有所揭示。

痒处二:同步和异步被人为地剥离。
  因为[1]的作者认为,操作者不应该关心到底是使用同步方式还是异步方式,这个问题应该由被操作对象自行决定。
  然而我觉得这种想法可能存在问题。
  问题一:被操作对象必须具有心智才能够决定到到底是同步还是异步,假如被操作对象是一张发票,那么这如何进行解释?
  问题二:事实上现实世界绝大多数情况下都是以异步的方式进行的。老师说“同学们翻开第78页”,然后无论同学们翻不翻书,老师都会继续说下去。上司向下属布置任务,也只有上司在不停的说,决不会每一句话都等下属确认“明白”之后才说下一句话。此外,现实世界如果需要进行同步,通常都是双方都有决定权的。老师说“同学们翻开第78页”,如果哪个同学还没有翻到那一页,深怕漏听了哪一句话,肯定得说“老师等一下,我还没有翻到那一页”。(高考复习给出重点复习内容的时候大家没少遇到这种情况吧,大学期末考试老师给出考点的时候也没有少遇到这种情况吧?)而上司如果深怕下属没有想明白某句话的时候,也会问下属“明白了没有?”或者“有没有什么问题?”,等到下属说“明白”或者“没问题”之后才继续说下去。
  问题三:其实现实世界当中的同步是通过等待他人“虚拟”出来的概念,正如计算机世界的异步是通过独立线程“虚拟”出来的概念。只不过目前对于计算机世界来说,被调用方向要主动要求调用方等待(或者不要等待)是一件较为困难的事情。除了软件方法当中缺乏这样的概念和工具之外,这也对调用方提出了更高的知识要求:原来只要知道“同步”(或者“异步”)调用对方的处理方法就能够完成工作,而现在却还需要知道如果对方要求“异步”(或者“同步”)的话怎么办。较为简单的办法是调用方继续假装一切按照同步的方式进行,但是这样的话有何意义呢?反过来也有类似的问题。当然,问题总是能够解决的,只是这种解决所付出的代价和获得的回报之间是否存在利益。很明显,以目前的技术条件不可能在所有的操作面前我们都能够从中获利。(假如是工作流引擎的话,可能性会比较大,毕竟工作流比较贴近现实,异步的操作实际上比较多。)

痒处三:无法自然地模拟现实世界中的感知能力(Sensebility)
  引文[1]作者认为,目前软件的问题在于缺乏感知能力。例如:“如果鸡块的颜色由肉色转变至金黄色,俺就必须做出相应的操作/处理:把鸡块捞出锅来。”而目前的软件设计更多的是有鸡块引发“熟了”这个事件,然后因为“我”订阅了鸡块的“熟了”事件因而此时被鸡块“踢了一脚”,“我”就立刻按照程序把鸡块捞出来。从这种描述看来,“我”并不是通过观察“鸡块”的颜色是否变黄来判断“鸡块”是否“熟了”。
  而我认为这种想法也很有问题(也许只是作者的例子举的不恰当)。

  首先,这里并不是没有进行观察,只是观察的是一个现实世界当中不存在的观察量。假如我换一种方法,变成订阅“鸡块”的颜色值变化事件(BackColorChanged),当颜色值发生改变的时候鸡块就引发事件。而如果“我”发现“鸡块”的颜色偏黄到一定的程度((BackColor.R + BackColor.G)/BackColor.B > threashold),“我”就捞起“鸡块”。怎么样,现在改成观察鸡块是否变黄来决定鸡块是否该捞起来了吧?可是你一定还是觉得不对,因为根本原因不在观察什么,而在于“我”被鸡块“踢了一脚”。
  在现实世界如果你被鸡块踢了一脚告诉你该捞起来了,你也许会觉得不爽,但在计算机的世界未必如此。假如你给KFC开发一个自动捞鸡块的机器,客户一定期望着鸡块熟了的时候能够踢机器一脚,让它立刻把鸡块捞起来。而且在可以有直接的、积极的、主动的方法进行沟通的情况下,为什么非得要屏蔽这一方法,舍近求远舍本求末的采取间接的、消极的、被动的方法进行沟通呢?现实世界的鸡块之所以不会再熟了的时候踢你一脚,是因为现实世界的鸡块确实是死物,并不是你不期待它会踢你一脚。如果鸡块炸糊了,你会感到更不爽的。再举一个例子,为什么现实世界的烧水的水壶还带一个“哨子”,水烧开了就叫呢?就是因为实际上人们期望水烧开的时候水会“踢”你一脚,让你赶紧把火给关了。虽然水是死的,我们却希望让它活过来,不是吗?
  因此我觉得这里是本末倒置了:很多时候我们之所以希望贴近现实,只是因为我们期望它能够跟现实当中那样方便;而如果现实当中的情况不符合我们的期望的时候,我们就不应该按照现实中的那样去做。

  其次,这个例子之所以能够引起我们的感触,是因为我们需要对“鸡块”的颜色做出判断,但是计算机竟然不需要。而且现实当中每个人对于“鸡块”的颜色变成什么样了才算熟,并没有一致的答案。其根本原因在于现实世界有很多很多的“量”,大体可以分为两种“可观测量”和“不可观测量”。鸡块的便面颜色属于可观测量,是否熟了属于不可观测量。于是我们会面对根据可观测量猜测不可观测量的问题,而每个人之所以会有不同的答案,原因在于猜测过程当中需要用到很多知识,也包括经验。每个人的知识不同,因此猜测的准确度就不可能相同。在这个例子当中,我们的根本任务是根据不可观测量的变化作为进一步操作的依据,也就是说“如果鸡块熟了,就捞起来”。但是“熟了”这个量不可直接测得,因此只好根据可观测量猜测不可观测量的值。可是如果现在给你一个神奇设备,能够直接告诉你鸡块“熟了”,你还会多此一举去判断鸡块的颜色吗?也许会看一下,以防设备坏了不知道,但是如果设备没坏,我想你是不会费神去根据鸡块颜色来判断鸡块是否熟了。
  简而言之,如果软件中的鸡块能够提供“熟了”的信息,就不应该屏蔽它然后提供“表面颜色”来让厨师猜测,此举舍近求远,浪费脑细胞——除非你在设计一款猜谜游戏,玩家扮演的是“厨师”这个角色……
  当然,从另一个角度看,我们也确实可以看到,现在常见的商业软件方法并没有“不可观测量”这个概念(但是在科学计算里面经常可以看到,不过也不是作为软件设计概念存在的),更不要说对此有什么解决方案了。如果我们正在设计的部分属于跟现实世界进行互动的部分,那么这一概念是不能够忽略的。对于工作流引擎来说就是这样的一种情况——什么时候属于“完成”了或者“出状况”了,有的时候并没有一个直接可观测得标志能够表明这一情况。如果软件足够智能,就应该考虑根据可观测量对不可观测量进行猜测的能力。

  最后,作者在文中接近最后所提出的一个说法,我想要提出比较强烈的异议。作者说:
  “在现有的delegate解决方案中,我们只能针对实例(Instance)进行注册,而不可以针对类型(Type)进行注册。……如果能够提供针对类型的注册机制,只要将俺的后续操作到鸡块类上注册一次,在感知范围内的所有鸡块,管他是十块还是二十块,都能被俺感知到颜色上的变化并执行正确的后续操作,这样会来的更简洁,更自然。
  首先,至少在.NET里面类型上面完全可以有delegate/event,比如说AppDomain.UnhandledExceptions就是类型上的事件,针对类型进行注册在.NET里面是完全可能的,并且不难看到。因此说这个并不是OO的问题,顶多属于某种语言内部的问题。其次,这种通过注册到某个类上面来对所有正在炸的鸡块进行观测,是一种很不明智的做法。因为这不属于类型上面的知识范畴,而应该属于某个锅以内的鸡块集合的范畴。假如现在有三个锅正在炸鸡块,那引文[1]作者的想法可就得挨屁股了。

  不过话又说回来了,我非常认同作者说的其中一点,那就是:不应该由鸡块决定什么时候被捞起来,而应该由厨师来决定,尤其是要根据他自己所处环境的其他状况来决定——海啸来了还管鸡块干什么,赶紧逃命吧!这是目前的模型所欠缺的,不管厨师的情况如何,鸡块熟了都会引起厨师的相应动作。当然了,你可以这么写:

PriorityQueue needToBeFishedOut = new PriorityQueue();

private void Chef_ChickenCooked(object sender, EventArg e)
{
    needToBeFishedOut.Enqueue(sender);
    needToBeFishedOut.BringUpPriority();
}

然后再弄一个线程或者计时器进行相应的处理。然而正如作者所言,很不爽。好比让你用C来写真正OO的程序,用asm来写网页,或者用小刀来刻硬盘。工具不对,做起来既费劲又不规范,因此Agent-OP确实需要发展。

最后,我还是要补充一句,OOP和Agent-OP属于不同层次的概念,因此Agent-OP和OOP的关系更像是Class和Method的关系,前者不可能替代后者,并且应该在后者的基础上发展。(我甚至觉得Agent-OP已经不属于语言范畴的东西了,不一定会出现像OO出现之后,世界上所有语言都向OO的方向发展那样的壮观场景了。)

posted @ | Feedback (7) | Filed Under [ 其他 ]

Wednesday, March 09, 2005

首先感谢拓荒者为我们提供了错误样本。其次感谢kaneboy把这个错误报告上去,并告诉我们这却是一个Bug,并且知道这个Bug在桌面上的所有x86版本的.NET上面都存在。这个Bug预计在下一个版本的.NET 2.0 beta里面得到修正,已有版本的SP就不知道什么时候才有了……这个Bug到底有多严重,见仁见智吧,不想在这里做评论。这里写一下只是让闲得无聊的人见识一下这个奇异现象,或者让真的觉得威胁到自己软件安全的人注意一下。
其次希望微软能给我奖金,或者至少稿费。


该错误在一般使用的情况下很少遇到,但在非常特殊的使用方式下才会产生,尤其在您特意使用一些C#的副作用的时候更容易产生。不过如果遇到了,说不定真是会损失惨重。(我个人感觉这种错误跟Intel多年前奔腾芯片的浮点错误非常神似。)

 

错误简述:

如果您的程序:
1、在某个整形变量上面进行不带检查的加法操作(unchecked,默认的行为方式,不包括减法、乘法、除法操作),并且产生溢出(溢出后数值必然是负值),并且
2、立刻紧跟在该加法操作后面判断是否小于常量零(加完之后有乘除或者函数调用等,或者判断的是某个变量里面的零,或者判断的是小于其他常量值等,都不符合该条件),并且
3、在该函数当中使用了该变量的引用,例如:a.XXXX() 或者 AnotherFunction(ref a)。

或者与此相反的:
1、……减法……(……正值)……
2、……大于常量零……
3、……

则会引起上述第二步的判断与我们的期望不符——似乎该数大于等于0,并且因此没有执行该条件分支的语句或者语句块。



下面是错误重现:

1、在C#里新建一个Console项目
2、插入下列代码:

 class Test
 {
  static void Main()
  {
   int a = 0x79de61c0; //2044617152;
   a +=    0x12345678;    
   //a 应为 0x8c12b838;  //-1944930248
 
   if( a < 0 ) a = -a;
 
   System.Console.WriteLine( a );
   string str2 = a.ToString();
   Console.ReadLine();
  }
 }

3、运行后发现,if(a<0) a = -a; 这一句话出现瑕疵,a < 0 测试出错,并因此没有执行后续的  a = -a 语句。

当我们去掉 string str2 = a.ToString(); 这一句话之后,错误消失。


下面是对该问题的具体分析:

该问题实质上是由于JIT引擎翻译逻辑有瑕疵引起的。当注释掉string str2 = a.ToString() 之后,我们调试时打开反编译窗口以及寄存器窗口。在寄存器窗口上面点击右键,选上“标志”。此时我们可以看到:



请注意图片黄色箭头处,if(a < 0) 实际上被翻译成jns 0016。jns机器指令的含义是,如果不是负数则跳转,实际上判断的是“符号标志”,也就是途中红圈圈上的"PL",这个标志位(以及其他一些标志位)由上一个指令add产生(这是该指令的副作用)。 由于符号为负,并没有条件转移,因此能够执行下一句a = -a (也就是neg esi)。但是请注意图中另外一个寄存器标志OV,该标志表示“溢出”。很明显我们的代码是因为相加溢出才导致结果变为负数,所以该标志位被置位。


当我们再去掉string str2 = a.ToString(); 并运行之后,我们可以看到:



注意图中红圈处,原来的jns指令现在被改为jge指令。jge机器指令的含义是,如果大于等于则跳转,实际上判断的是“OV”、“PL”,相当于if ((OV ^ PL) == false) goto xxx。也就是说,多关注了一个OV标志。

很明显由于前面的溢出,造成OV标志置位,因此条件转移成立,结果没有执行后续的neg esi。所以我们从源代码的角度看,似乎此时变量a的值是非负数,这跟该数为负数的事实不符。

尽管从源代码的角度看,似乎没有任何与if(a < 0)有关系的改动,实际上由于我不清楚的理由,当该函数中使用到了a的引用,结果造成了if(a < 0)翻译成机器指令的不同,进而对一些副作用的反应不相同。

此例当中对a的引用是a.ToString(),实际上如果您使用AnotherFunction(ref a)替代这一句话,也会引起相同的问题。与此相反的减法操作的问题,原因和解释类似。


该问题的解决方法:
1、尽可能不要利用C#相加/减溢出后变成负数的副作用,例如用下列方式避免副作用:
  checked
  {
     a += 1234;
  }

2、如果您确信相加后溢出的副作用是必须的,那么请采取下列措施避免该错误:
  a += 1234;
  b = a;
  if (b < 0)
  {
    a = -a;
  }

注意,这个并非微软的官方解决方法,目前我并不清楚微软的KB里面是否有该问题的纪录。

p.s.: 有人告诉我,不一定非要有对a的引用才会引起问题,具体我也不研究了,反正对a进行引用一定出问题。(也许是对其他的引用产生了ldloca之类的il代码吧,没有研究过)

posted @ | Feedback (16) | Filed Under [ .NET 技术内幕 ]

Thursday, December 09, 2004

好久没有上来写点什么东西了,也有一段时间没有到博客堂拨客园上面来采风了,今天一上来就看到这个文章:
数据类型的BUG还是???

里面罗列了一些问题,也许我能略解一二。比如说问题二:

dim ss as double

ss = 400*1000

在VB6中,报越界!!

ss = 400*100000没有异常

其实是这样的,VB6里面对于常数,如果没有带数据类型标志符(例如#)或者小数点,就认为是整数。而对于实际上是什么整数,则根据最小化原则,认为400和1000同为16位带符号整形。而数值计算结果的数据类型和操作数中表示范围最大的相一致(其实大部分语言都是这么定义的。ps:对于VB6及以下版本,似乎没有应用常量传播,要到计算里面才会出错),很明显400*1000的计算结果超出16位带符号整形的表示范围,报越界。而400*100000里面后者被认为是32位带符号数值,因此计算结果也是32位带符号数值,所以不会越界。

请注意,大部分语言的计算过程是有一个中间计算结果的,这个结果跟最终承载变量没有关系,而跟该语言的运算法则相关。在本例当中无论ss被定义为double、long还是别的数值类型,都必然会引发越界,这是由VB6里面的语言定义所引起的。而中间计算结果要经过一个转换过程才能够得到最终变量的数据类型,一般的基本数据类型之间都有“隐式”转换,有的是强制转换,这个一般由语言本身所定义。例如在VB6里面,几乎所有数值类型之间都能够自由的进行隐式转换,但是在C#里面,浮点数转换为整形数字的时候就必须要强制转换。

当然,从某种角度来讲,所有数值类型之间能够自由的隐式是VB6语言定义本身的缺陷,因为这样可能会引发很多“看不见”的问题。但是这实际上是VB6语言本身的定义,而不是设计人员无意识的或者不期望的结果,所以我宁愿称之为Fault也不愿意说是Bug。

可是大坏蛋却说.NET里面:
 double ss;
 int firstInt = 2147483646;
 int secondInt = 2;
 ss = firstInt + secondInt;
 Console.WriteLine(ss);

结果:ss = -2147483648

似乎对这个现象有点意见。首先,还是那个原因,计算是有中间结果的,中间结果的类型在这里仍然是int。其次,要追溯C语言本身的处理方式,在C语言里面不会对整形的上下界超界产生任何疑问,甚至不会报错。因为这个被认为是C语言的“特性”之一,C#“号称”继承了C/C++,那自然也会尽可能继承这些传统习惯,因此他就作为语法规范里面的一部分了,无可厚非。而事实上这也不是.NET Framework的功劳,而仅仅是C#的定义而已。因为在VB.NET里面,这会产生异常的。因为在C#的编译器对整数加减法使用的是不带检验的IL指令,而VB.NET则使用的是待检验的IL指令。比如C#使用的是add指令,而VB.NET则使用的是add.ovf指令。当然,这是在最普遍的代码编写方式,以及默认的语言参数下面而言的。

如果有什么疑问,请尝试下述代码:

 int ss;
 int firstInt = 2147483646;
 int secondInt = 2;
 ss = firstInt + secondInt;
 Console.WriteLine(ss);

呵呵,现在再请没有疑问的尝试下述代码:
int ss = int.MaxValue + 2;

回过头来我们再看看第一个问题:

dim ss as double

ss = 194268.02 – 194268

肉眼可以判断结果为0.02,而VB中计算的结果:ss = 0.199999999895226E-02

ss = 1.2 - 1 VB计算的结果为:0.2


要知道这个问题的答案,我们首先要看看这里的浮点数到底是什么浮点数。在.NET Framework里面(以及VB/VC等)遵循的是IEEE标准,那么为什么0.02不是0.02了呢?其实这个在IEEE里面可以找到一个快速的解答。那么为什么后面一个计算会是正确的呢?那其实是因为“精度”足够,使得你认为它就是0.2。事实上IEEE浮点数永远不可能精确等于2的n次幂相加所构成的数值(比如1.375 = 20 + 2-2 + 2-3,我“简称”这种数字为“可被2整除的数字”。),除非IEEE更改了他的标准。(关于IEEE浮点数的定义可以参考这里。)顺带给出double的1.2和0.2的十六进制编码:
1.2 = IEEE_double(3FF3 3333 3333 3333)
0.2 = IEEE_double(3FC9 9999 9999 999A)
而1.2-1的运算结果却是 IEEE_double(3FC9 9999 9999 9998),看到了吗?其实1.2 - 1并不等于0.2的。而事实上IEEE浮点运算即使是在“可被2整除”的数字之间进行,通常都会有误差的,这主要源于精度丢失。前面的1.2 - 1的误差并不属于这个范畴,这主要是由于操作数本身无法被精确表示而造成的(虽然也有精度丢失的原因)。可以说精度不丢失的情况是相当特殊的,比如说完全相等的两个数相加减,乘、除以2的整倍或者正负1以及0,和0或者“非数字”之间的计算,等等。

所以说这些问题千万不要往MS的头上扣,也不是MS所能够改变得了的。

posted @ | Feedback (13) | Filed Under [ .NET 技术内幕 ]

Thursday, November 18, 2004

今天看到朋友wayfarer写的一篇文章,大概是关于protected的“保护性”问题的,看过之后内心有些想法想与大家分享,如果大家不嫌弃,敬请往下看。

拍脑壳所想之
 
  ——戏言面向对象


说到protected这个词,我不可避免的就会想到一个概念——面向对象。那么什么是面向对象呢?其实我个人认为面向对象这个概念是一直在发展变化的,到了今天,面向对象这个词也许让它叫做面向抽象更加贴切。在刚刚建立面向对象这个概念的时候,大概连创造者对于到底什么是面向对象都不是很清楚。要搞清楚面向对象(编程,或者设计)是什么,也许得看看过去的软件代码都是什么样的。



I.公元前
软件开发在最初的十几二十年里面,基本上就是面向过程的。面向过程的核心内容有两项,一个是控制流,另外一个就是数据流。在这一个时期里面,软件界最大的发展估计是数据结构与算法这两个“科目”了,这两者分别对应着两个“流”。在面向过程的软件代码里面,执行主体是过程或者函数。一个过程所代表的就是一个动作,动作的对象(这里还不是面向对象的对象)是一些数据,数据也许通过参数得到,也许通过全局变量得到,还有一些常量或者预定义值。如果我们仔细想一下,就会发现这是一个“动宾”结构的体系,比如说Basic里面比较著名的“Line (x1, y1) -(x2, y2)”,翻译成自然语言就是“画一条(x1,y1)到(x2,y2)的直线”。类似的例子还有很多,比如C语言里面的“printf("%s\r\n", "Hello world!");”。

可是主语在哪里?



II.创世纪
面向过程的代码里面并没有突出一个主语,很多时候这个主语也并非不存在,就像上面的例子里面,主语就是一个屏幕。可是如果我们需要往打印机里面画一条直线呢?(或者打印一个"Hello world"。)在面向过程的代码里面,我们就不得不自己写一个PrintLine的函数。(C语言往文件里面些东西就是fprintf。)如果我们要往远程设备上画一条直线,那还要写一个RemoteLine,如果……不需要我多说,您也会觉得麻烦。围绕着这样一个问题,人们就开始思考:是否能够把主语明确的给写出来?是否能够让我们少做一点重复性的工作?后来就有了面向对象这个东西,在面向对象是一个“主谓宾”结构的世界,绝大多数东西都有一个主语,比如我们所熟悉的“g.DrawLine(pen, pt1, pt2);”,由于我们有了“主语”,我们就可以让不同的东西,用相似的方法做相似的事情。如果光是把g换成h,仅仅解决了“在这个窗口画”与“在那个窗口画”的问题,如果我们希望他能够在其他类型的空间上画,我们还需要容许主语的类型可以不完全相同。但我们要解决的更多问题还是概念相同之处,例如打印机的g和屏幕的g都能够画线,因此有了诸如继承、封装等概念。这就是面向对象的一切了吗?



III.改革开放
随着面向对象概念的诞生,春风沐浴大地。正如上帝说要有光,于是有了光。上帝说要有毒蛇,于是有了毒蛇,上帝说要有苹果,于是有了苹果,结果亚当和夏娃吃了这个上帝创造出来的苹果受到了上帝的“惩罚”。真不明白,既然上帝不希望亚当和夏娃吃这个苹果,为什么还要创造这么一个东西?其实上帝创造这个苹果当然是不希望他们“吃”这个苹果,创造这个苹果实际上是为了产生浪漫的爱情以及其后千秋万代的动人故事。如果你把这个苹果仅仅看成是吃的,那么接下来你看到的就是痛苦的惩罚。如果你看到的是背后动人的故事,那么浪漫甜蜜等美好之辞就会充满你的大脑。

面向对象也一样,他的核心意义并不在于你把东西封装成什么样了,不在于有什么东西被继承出来了,最重要的是他容许我们用抽象的方式来构建一个软件。比如当我们写代码写到:

stream.Write(buff, 4, buff.Length - 4);
或者
hashbuff = hasher.ComputeHash(buff);

我们是否需要关心stream到底是什么,hasher用的又是什么算法呢?如果我们由始至终,在做相应的东西的操作都用相同的stream对象和hasher对象,任务是否都应当能够正确完成呢?应该是能够正确完成的,因为这正是我们的期待。如果让我们来设计某一个stream,是否应该从这个角度去考虑如何设计这一个类呢?如果我们定义这个stream变量,是否应该更抽象一点呢?考虑这么一个函数:
void DoSomething(FileStream stream, MD5CryptoServiceProvider hasher, byte[] buff) {...}

如果写成如下形式将会更加灵活,也更加符合面向对象(面向抽象)的真实含义:
void DoSomething(Stream stream, HashAlgorithm hasher, byte[] buff) {...}

换句话说,所有的封装、继承、接口等等,实际上是为了提供抽象能力而存在的。如果我们把protected当作保护“某些方法的存在”这个秘密的话,那就大错特错了。保护这些秘密严格说来应该是密码学的职责,而不是面向对象的职责。



IV.回顾历史
面向对象的核心是面向抽象,但我们看到,实际发展的过程并非如此。我们在过去有着太多错误的概念了,比如说这个面向对象技术的面向对象,就太容易让我们认为,这项技术的核心就是面向对象。于是很多时候我们写一个“面向对象”的程序充斥的过度的对象,泛滥的继承,以及不知道为什么的封装。并且不少开发者,包括我在内,都曾经认为所谓的面向对象就是把一些要素抽象成对象,进行封装,然后从某个基类派生出万物。好比有一个基类叫做物体,派生出活物与死物,活物派生出细菌病毒植物动物,动物里面有猴鸡狗猪和人,人里面有张三李四王二麻子(还有个娃)。
没错,面向对象当然得包括这些,但是这不是全部,更不是根本。根本就是在于我们写某些东西的时候,不需要关心具体的对象是什么,只需要知道至少它应该是一个什么。比如上一节当中的例子,DoSomething只需要知道stream是一个流,而hasher是一个哈希算法提供者就够了。至于具体提供的是什么样的流和哈希算法,则不应当是我们关心的,而是使用我们这段代码的用户所关心的。如此一来,我们就可以在设计这一段我们所关心的功能的时候,不需要考虑过多的、过于具体的、不断变化的问题。
仔细想想,我们是否真的已经明白了面向对象的核心所在呢?



V.封装保护的是什么
面向对象的封装并非保护你的秘密,而是防止被错误使用,是为了明确划分问题的界限。就“保护”这个词而言,更进一步的讲,它并非对使用该对象的用户(下面称为用户)做出使用某个成员的授权,而是对延展该类的设计人员(下面称为设计人员)做出延展问题领域的授权。现在让我们回过头来看一下wayfarer所写的例子:

class Base
    
{
        
protected void Print()
        
{
            Console.Write(
"This is protected method in Base Class!");
        }

    }


    
class Derived:Base
    
{
        
public new void Print()
        
{
            
base.Print();
        }

    }
class OtherClass
    
{
        
        [STAThread]
        
static void Main(string[] args)
        
{
            Derived d 
= new Derived();
            d.Print();
            Console.ReadLine();
        }

    }


这个例子确实是非常容易迷惑人的,曾经,我也被这样的问题所困扰。在解决这个困扰之前我们首先要弄清楚下面两个问题:
protected是什么?new又是什么?

protected 很好回答,他表明该成员容许在派生类当中被使用,但不允许使用本类对象的用户代码直接使用。实际上是对设计人员的有限度授权,和对用户的拒绝授权。
而new也并非难以回答,比如说:他是为了在没有override的情况下造成一种被重写了的假象。如果您真的这么认为,那就掉入了幻觉的漩涡当中去了。事实上new的作用并非一个trick,让你可以造成各种各样的假象,或者企图绕过某些使用与设计的授权。new的作用仅仅是为了解决一个命名冲突的问题,也就是说new所指定的成员实际上与基类的同名成员毫无干系,只是非常抱歉的跟他重名了只好声明此Print非彼Print。如果您真的企图用new来制造trick假象的话,终究是要撞掉你的门牙的。在我举出“撞掉你的门牙”的例子之前,请容许我首先给出一个正确使用new关键字的场景。

不知道各位有没有真正的研究过.NET Framework里面interface呢?如果研究过,对于下面的这个问题应当不是非常难以回答。

    public interface IFoo
    
{
        
bool Bubble();
    }


    
public class Boo
    
{
        
public void Bubble()
        
{
        }

    }


    
public class Foo : Boo, IFoo
    
{
        
public bool Bubble()
        
{
        }

    }

上面这个代码会在Foo的Bubble函数上面产生一个警告,但是仍然能够编译通过。为什么能够编译通过呢?这个问题留给读者自己琢磨了。解决这个警告的办法有两个:一个是显式实现接口IFoo;可是如果我不希望通过显式的方式来实现该接口,那么就只能够在Foo的Bubble函数前面添加一个new修饰符,告诉编译器我知道他们有冲突,但是我还是希望选择用这种方式来完成它们。这两个Bubble相同的名称给我们一种它们之间有什么联系的错觉,事实上 new bool Bubble() 与 bool new_Bubble() 的含义接近,和Boo里面的void Bubble可以看作毫无关系。如果你觉得有关系的话,那么下面的我将举出一个例子让你碰一鼻子灰。

    public class Boo
    
{
        
public void Bubble()
        
{
            Console.WriteLine(
"Boo sheet");
        }

    }


    
public class Foo : Boo
    
{
        
public new void Bubble()
        
{
            Console.WriteLine(
"Foo sheet");
        }

    }


    
class Program
    
{
        
static void Main(string[] args)
        
{
            Foo obj 
= new Foo();
            Test(obj);
            Console.ReadLine();
        }


        
static void Test(Boo obj)
        
{
            obj.Bubble();
        }

    }


你猜你会说Boo sheet呢,还是Foo sheet?为什么会这样也请自个儿思考一下。

wayfarer所举的那个例子,看起来确实容易造成困惑,或者会让大家觉得这里有一个暴露protected函数的bug,但事实上并非如此。还记不记得前面我说过了,protected是一个授权问题,而非保密问题,这一个说法应该能部分解决您的困惑。而上面的Boo sheet例子表明,实际上你并没有暴露那个protected函数,因为你仍然无法直接从一个指向Derived实例的Base变量上面寻求到使用Print函数的方法。如果您还记得我前面说过的面向抽象这个概念,也应该意识到,您写的Derived类只是对Base类的补充,而用户一般应该用Base变量来使用您的对象,而不是Derived变量,除非他认为他需要使用Derived提供而Base不提供的功能。如果说我不使用new关键字,而是把Derived的Print函数命名为PrintBase,那是否算是会引起“暴露”基类成员的Bug呢?

显然不是。此时如果用Base变量仍然无法访问Print,用Derived变量则仍然可以访问到PrintBase(并最终调用Print)。还记得派生是为了延展问题领域的边界吗?这里将Base的一个受保护函数暴露出来,就是延展了问题领域的边界。设计Base的人认为,Print的功能是对象的内部事务,而Derived的设计人员则认为Print功能应该是外部与内部之间的事务,此时是否仍叫Print已不重要了。(P.S.: 容许派生类使用,却不允许被暴露,这是根本不可能的事情,即使new关键字不存在也一样。而真正能够称之为“暴露”的是反射邦定/反射调用等。可惜我们还是不能够称之为Bug,因为这正是“反射”这个设计所期望的功能,而非不小心造成的有害副作用。)

说到这里,我想protected和new的问题应该已经讲完了。还有什么疑惑吗?


VI.回到未来
回过头来再说说面向抽象,以及面向接口。 前面已经提到了面向抽象了,不知道大家是否有更多的感想。抽象到了头是什么?当然不是什么都没有,不是虚空太极。抽象的本质是描述某个主语能够完成一组什么动作,这些动作构成了什么样的功能。如果我们从这样的一个角度去想,就会发现接口能够很好的完成这样的任务。比如说IList,它表达的是“一个列表”这样的抽象,这个抽象能够提供一组相应的动作,比如"object this[int index];" 能够取出或设置列表当中的第n项内容。只有拥有IList接口所定义的成员,才能够表明这个物体确实能够称之为“一个列表”。“服务”这个词也许能够更加深刻的表达上述的含义:
class ArrayList : IList {...}
这样的定义表明,ArrayList提供“列表”相关的服务。如果我们在定义变量和参数的时候更多的使用接口,而不是具体的类,那么我们的代码将拥有更大的自由度。这个时候我们关心的事某个对象是否能够提供我所需要的服务,而不关心他到底是什么。

在面向对象刚刚开始的时候,我们在这个方面走进了一个误区,就是用多继承来解决上面的这个需求。比如我们可以在C++里面看到Stream派生自IStream和OStream,这么做也能够解决问题,也许同样能够表达“服务”这个含义。但是我觉得这样做还是会引起许多不必要的麻烦,比如同名称冲突等。现代的理论甚至直接告诉我们继承他不是一个好东西,如果能够用引用来代替继承,那就不要继承,如DesignPattern里面的Decorate等。


真正未来的理论是什么,我不知道。但是至少我看到面向接口的设计思想比“面向对象”要更为先进,而目前真正能够这样思考的人远比知道如何封装继承的人要少得多。从这个角度讲,那也算是未来的技术,至少是未来需要普及的技术。(现在COM不就是这样一种思想吗?)

posted @ | Feedback (24) | Filed Under [ .NET 技术内幕 Design & Architecture ]

Saturday, November 13, 2004

前一段时间已经发现CF在载入资源的时候会怪怪的,但是这一段时间都不曾记起要对这个问题研究一下。最近又发现这个问题了,实在是恼火。俗话说择日不如撞日(粤语),唉,就今天啦。这个问题是在VS2k5里面调试的时候才发现的,以前在VS2k3里面一直都不知道有这么一个问题存在。事情是这样的:

有一次在VS2k5里面调试运行一个SmartPhone的程序的时候,发现突然在Output窗口里面显示了好几个First Chance Exception,仔细一看发现有两个FileNotFoundException,以及一大堆的UnathorizedException。而这些异常却没有引起中断,也就是说在程序的某一个部分被try...catch截获了。但问题是我的代码里面根本就没有任何的try...catch,不应该呀?后来仔细想想,又想到using也截获异常,所以一下子下不了定论。而我这个程序只有关于画图方面的代码,因此问题应该在画图相关的部分。我立刻想到有两种可能性,第一种是加载资源的时候引起了异常,另外一种可能是画图代码由一些可能引起异常、但是系统能够处理的参数。由于没有时间,开始的时候就没有仔细探索,反正能够正常使用。

而昨天呢,我稍微研究了一下,发现问题确实是出在加载资源的部分。当你的SmartDevice程序里面包含了图片资源的话,并且通过资源管理器的GetObject获取这项资源的话,就会在内部引发一系列的异常——第一次引发两个,第二次引发另外两个,后面的就每次引发一个异常。简单的跟踪发现,问题可能出在本地化方面的问题。似乎在GetObject的控制流当中的某一个环节,会检查你指定的“文化(CultureInfo,不知道中文术语是否这么讲)”,然后找到相应的资源文件载入。这个资源文件是干什么的我没有仔细看,这只是一个粗略的分析,估计是用来显示出错信息的。由于没有指定CultureInfo,或者指定CultureInfo的资源文件没有被安装上来,结果就引发了FileNotFoundException了。至于后面的UnaotherizedException,我估计就是一开始引发了FileNotFound之后,某些初始化步骤出现问题的副作用引起的。

这个问题也许值得花时间去了解清楚。大家都知道抛出一个Exception,无论代码有没有catch,时间开销都是相当大的。可以说如果你的程序有图片资源,其启动速度就会跟没有图片资源的相差非常大。大家可以试一下建立一个空白的程序,运行一下感受速度。然后在这个项目的Form上面添加3个PictureBox,设置它们的Image属性,分别换上不同的图片。再次运行的时候就会感到速度会有差异了(应该还好,不算很明显。)如果在VS2k5里面做这个实验就再好不过了,因为能够在Output窗口上面看到很明显的First chance exception项目的。这个CF如果跟桌面的.NET Frameword对比,真算不上是精品工程,越用就越多问题。无论如何,在正常使用的情况下抛出一个错误(虽然接住了),都是不正常的设计方式。

呵呵,不知道现在VS2k5的beta里面是否有中文的Rom了呢?如果没有的话,下一次来看我的解决方案,今天不在工作机器上面,很多东西没法写,怕不准确害死人。OK,今天就这些。

posted @ | Feedback (7) | Filed Under [ .NET CF ]

Thursday, November 11, 2004

不过从质量的角度讲,那就是另外一回事了。

   blogchina.com/     Go to http://www.blogchina.com/
www.blogchina.com

  

Average Traffic Rank: 516
Average Customer Review: 5 out of 5 stars Based on 3 reviews. Write a review.



  blogcn.com/     Go to http://www.blogcn.com/
www.blogcn.com

  

Average Traffic Rank: 843
Average Customer Review: 2.5 out of 5 stars Based on 2 reviews. Write a review.




   blogdriver.com/     Go to http://www.blogdriver.com/
www.blogdriver.com

  

Average Traffic Rank: 2,370



      Go to http://www.yourblog.org
www.yourblog.org

  

Average Traffic Rank: 2,436



  cnblogs.com/     Go to http://www.cnblogs.com/
www.cnblogs.com

  

Average Traffic Rank: 18,188



   blog.joycode.com/     Go to http://blog.joycode.com/
blog.joycode.com

  

Average Traffic Rank: 20,362



   seov.net/     Go to http://www.seov.net/
www.seov.net

  

Average Traffic Rank: 64,846

posted @ | Feedback (54) | Filed Under [ 其他 ]

最近逛新闻站点(非IT),有那么两件郁闷的事情不吐不快。
第一件事情是万恶的凤凰网,以前还算好好的,但是自从个把月之前全新改版之后,就出了一些让我难以忍受的恶心问题。首先是主页加载速度奇慢,然后是新闻页面加载更慢,经常是打开了之后两三分钟才会显示出来。如果连续打开多个新闻页面,甚至会出现莫名其妙的stack overflow at line 0的错误!稍微看了一下发现很多时候实际上整个html已经下载下来了,就是不知道他在干什么。也许里面include了另外一个什么页面或者脚本,但是出现stack overflow这样的错误实在是无法理喻。更加无法理喻的是,都这么长时间了都没有明显的改善,仿佛凤凰网的人从来就不检查一下自己的网页到底怎么样的。现在我经常是在打开一个新闻,等上两分钟,然后还是没有画面的话,只好按Alt-V、C来看源代码了。凤凰网就是凤凰卫视的官方网站:
http://www.phoenixtv.com/
(难道只是我这里的网络有毛病?)

同样让我异常怀疑到底是我这里的网络有毛病呢,还是网站有毛病的是下面这一幅截图:


我开始粗一看去,觉得没有什么,不就是Access Error。后来觉得不对,正规的说法应该是Access Denial吧?更加让我吃惊的是最后一行小字“dxxxxx fxxx out”,这个也太夸张了吧?这个是访问新浪的时候出现的错误,暂时不清楚是新浪修改了出错页面呢,还是IIS的隐藏错误呢,或者是我这里的网络提供商插入的数据呢,抑或是我们内部网关的问题?
但是无论如何这都不是我的错,我只想上上网,结果就……

谁也遇到过这样的问题吗?原因有人知晓吗?请恕我孤陋寡闻。

posted @ | Feedback (27) |

Monday, November 01, 2004

1、读一文件2M大,与读144个共200K文件同速;
2、快排序远比删除要慢(仅供参考)。

posted @ | Feedback (9) | Filed Under [ .NET CF ]

Sunday, October 31, 2004