代码改变世界

谈吉日嘎拉的《白话反射技术》及其他(技术篇)

2009-10-16 19:16  Jeffrey Zhao  阅读(26245)  评论(88编辑  收藏  举报

社区又掀起了腥风血雨,这次又是吉日嘎拉这一博客园的众矢之的所引发的惨案。他的一篇《白话反射技术》发表之后,被包同学一篇文章狠狠地踩在脚底下,言辞之激烈令人罕见。从两片文章的内容与评论来看,大家的眼光似乎都没有集中在技术本身,而是针对个人在你来我往。有评论称这是“门派之争”,虽然看不出到底哪门哪派,但看上去也还真像那么一回事情。不过这真是技术社区该有的讨论氛围和方式?如果觉得吉日嘎拉在技术上有问题,难道不应该条条指出吗?既然没有人做这件事情,那么就还是我来吧,反正我写博客也成习惯了。

文章还是分两篇吧。先谈技术,再谈非技术——这样某些朋友也可以有选择地忽略某个部分。

反驳一个人的观点,我认为最简单而直接的方式是逐条批驳,必要的时候可以引用原文。首先,吉日写到:

使用反射有几个误区:反射的性能慢?其实未必反射的性能是慢的,说不定有些场合,反射的性能是更快更高效的,不用它的优点,用了他的弱点,那就无法达到高效的目的了,文章的结尾也说说我的观点。

我同意他的部分观点。“反射性能差”的确是相对的,例如反射调用一个方法,访问一个属性的确比直接访问要慢上许多。但是对于一个普通应用程序来说,反射几乎不会成为性能瓶颈,因为性能瓶颈往往是由外部IO操作造成的。例如,一个数据库访问,一个Web Service调用,就算是有朋友认为的Remoting这个性能非常高的分布式调用方式,它的通信性能也远比反射要慢好几个数量级。假设一个反射消耗的时间是0.0001秒,那么它的确比直接调用消耗的0.0000001秒慢上一千倍,但是它还是可以在1秒钟执行1万次,不是吗?但是我们平时的SQL查询,有哪个可以如此高效?

此外,我们现在也有很多方法可以加快反射调用的性能,例如我曾经写过的一个简单小类库,可以保持反射调用的方法不变(如只是把Invoke方法调用变成了FastInvoke),但是性能也接近了直接调用。这是因为使用了Emit动态生成了IL代码,这样在调用的时候便和直接调用如出一辙。对于需要大量使用反射的场景,例如NHibernate需要通过反射为属性一个一个赋值,那么它一般也会使用类似的机制来提高性能。

至于是否有“某些场合”反射的性能是更快更高效的,我想不出来。不过如果只是“利用了它的弱点”,那一定是不合适的,这就是所谓的“滥用”。

我在日常开发中反射常用的几个环节,其实架构师架构系统反射等用得多,程序员日常开发里,其实不懂也没啥大不了的。

我“直觉上”不同意这种使用“架构师”、“架构系统”来划分反射作用适用性的方式,但事实上我也无法在这方面说出个所以然来。或者说,我只能说一句废话:“该用反射的时候就使用反射”,普通程序员还是一定要懂反射的。

在很多时候反射也是唯一的选择。为什么我们会选择使用反射?因为我们没有办法在编译期通过静态绑定的方式来确定我们要调用的对象。例如一个ORM框架,它要面对的是通用的模型,此时无论是方法也好属性也罢都是随应用场景而改变的,这种完全需要动态绑定的场景下自然需要运用反射。还例如插件系统,在完全不知道外部插件究竟是什么东西的情况下,是一定无法在编译期确定的,因此会使用反射进行加载。其实,包同学的反驳文章里也是持这种观点的:

最后,不要怀疑反射的应用,三层架构中的数据层和DB的Mapping,AddIn架构,都离不开反射。这个话题说起来就大了,没有几年的项目实际是感受不到的。

“数据层和DB的Mapping”或是“AddIn架构”,其实都是架构层面的内容,刚好符合日吉的观点“其实架构师架构系统反射等用得多”,那么这架究竟是怎么吵起来的呢?

接下来吉日展示了他使用反射的常见场景。其中提到:

两个类,需要互相调用了,不能直接进行互相引用了,那不是死循环了?例如我这里2个包里的窗体之间,需要互相调用,就用了反射技术,直接动态的从dll 包里把相应的窗体呼叫出来,当然我也不想用反射但是不用不行了,没有其他更好的解决方法了,我不是为了玩技术用技术,而是实际开发中遇到问题了不用不行了,才用反射技术。

……

我们在开发大型软件项目时经常会遇到,系统很庞大了有几百M的代码了,主程序启动时,总不能把这些都引用了吧?全部加载在内存里?那程序的启动速度,不知道会不会慢如老牛推车了?这时候也会用一些反射技术等,用到哪个窗体,就动态加载哪个那个窗体,总感觉比较清爽一些。

在我看来,这段话可谓完全是错误的

两个类,如果需要互相引用,那也就是产生了双向依赖。如果这是设计上的问题,那就应该从设计的角度来解决。如果“直接动态从dll包里把相应的窗体呼叫出来”,这难道还不是一种双向依赖吗?难道因为“编译期”可以通过了,在“运行时”就解耦了?这只是把依赖关系给隐藏起来了,“总感觉清爽一些”,这平白无故的感觉要不得。这种反射的使用方式在我看来就是一种滥用,因为这完全可以在编译期通过静态绑定的方式来确定调用目标。如果使用反射,那么就无法由编译期来进行检查,这样项目的第一道防线也就丢失了。

根据吉日的观点:“系统有几百M的代码,主程序启动时把所有的代码都加载了”。事实上,CLR并不会这么做。如Essential .NET所云

The CLR loader is responsible for loading and initializing assemblies, modules, resources, and types. The CLR loader loads and initializes as little as it can get away with. Unlike the Win32 loader, the CLR loader does not resolve and automatically load the subordinate modules (or assemblies). Rather, the subordinate pieces are loaded on demand only if they are actually needed (as with Visual C++ 6.0's delay-load feature). This not only speeds up program initialization time but also reduces the amount of resources consumed by a running program.

事实上,CLR只有在真正需要某个程序集的时候才会进行加载,因此不会因为程序庞大而“启动速度慢如老牛推车”。

而接下来,吉日写的是大家最常见不过的“通过反射加载数据访问层来应对不同数据库”:

我只写一份商业逻辑,但是希望能跑在多种数据库上,我配套每种数据库的商业逻辑部分都相应的写一份,那我的工作量是加倍的,每个包都要进行测试、维护、升级、改进、调试、优化,世界超级大国美国也只能同时进行2个局部战争,你能同时谈3-4个女朋友,那我真服了有本事啊,一般会没多久就会穿帮了,维护几个数据库访问方法倒是不会工作量很大,相对来讲是有限的。

……

配置文件里,明确指出,我需要用什么数据库,什么连接串等。

……

我的数据库访问工厂里,按配置读取相应的数据库连接类、实例化相应的类。

……

这也是典型的使用方式,被无数人翻来覆去写过,被更多人翻来覆去使用过的方法。PetShop也是这种做法,因此虽然我不喜欢这个,但我也的确挑不出毛病来。

我说我不喜欢这种方式,是因为我认为这种数据访问层的写法过于机械,也比较难以使用,代码较多,经历了几个项目之后我被搞得非常疲惫。我目前比较欣赏的方式是基于ORM框架编写的数据访问层。我接下来也打算谈谈我理想中的设计,不过这里就不多谈了。

我发现,其实这也是包同学提出的反射场景之一……我真的搞不明白,这一架究竟是怎么吵起来的!?

这些用到的反射、系统架构时调试通过了,别人也根本没兴趣研究了,也不需要所有的人都折腾反射,偶尔需要用时,照葫芦画瓢就可以了,先copy然后past,然后修改几个名称什么的,然后run,运行正常了,就懒得管了,不行就dbug。

我们公司没几个人用到反射,公司里估计就2个人,其中一个也是用DNT的现成的,我这个是自己折腾出来的。实际工作中需要了,就用用,或者想学技术,就自己弄弄,没必要为了显示你技术都少厉害非牵扯个反射出来。

“没必要为了显示你技术多少厉害非牵扯个反射出来”,我同意。技术的确是为了用的,不是为了显摆的,但是这并不是“不去学反射”的理由。这也是我不喜欢吉日最主要的原因,就是他对于技术的一种“无所谓”的“将就”态度。一直听到这种说法,技术不用特地学,等用到了再学,边学边用。但是,如果你不去学技术,又如何可以得知哪个技术适合你目前的使用场景?有很多朋友说工作就是日复一日的重复,没有长进。这可能是多种原因造成的,但是我想如果抱着“不主动学习技术”的心态,那么不断重复还真不是“无法想象”的情况。

但也有朋友会回复说,有些人他的确不是热爱技术的人,也只喜欢过普通的日子,我和他们不是一个情况,无法理解。的确,我可能真的无法理解,因此我也只能不断地推广、表达、推广我对技术的理念,而不会,也无法把自己的观点强加给别人。

最后,吉日谈了他最讨厌的反射使用场景:

or mapping 什么的,一个类里的属性,方法通过循环读取出来,然后一个类的保存,读取,能自动实现什么的,若有10000个对象的,每个对象有50个属性,那得循环多少次?不是循环死人啊?还有就是 .net 的类型与数据库类型的转换匹配,null 的匹配等空日期的匹配转换,多种数据库类型的转换,我看了这样的代码,就恶心想吐,为了提高性能还需要延迟加载什么的搞得死去活来,我们不是搞研究玩技术的,把项目又快又好又简单的做好,客户满意,能及时拿到项目款就可以了,杀鸡用刀就可以了,别还先打一会儿太极,再品茶啥的,赶紧杀鸡啊,我看着都心急。

后来事实证明,代码生成器还是蛮好用的,不是靠反射循环搞定问题,而是通过机器来产生代码,这个的确比较好,我也喜欢。我也曾经用过很多乱八七糟的技术,现在懒得玩,懒得学习了,跟着微软屁股后面跑微软哪个成熟了,我就玩那个,平时关注一下发展动向,不是微软的能不用就不用了,我不想折腾了。

单个技术,都是非常好的,但是组织到一起就容易乱套了,不伦不类,而且把性能优化到最好需要更高的水平了。

ORM是大量使用了反射,它的代码的确也“不甚美观”,因为反射本身是很丑陋(好吧,应该说是“麻烦”)的调用方式(C# 4.0的dynamic关键字则很好的解决了这个问题)。但是这不正常吗?我曾经在互联网上看到过这个说法:“任何美丽的接口后面都有丑陋的实现”,美妙的接口,好用API,它的目的就是帮你在项目中避免“亲自”操作这些丑陋的内容。

ORM框架的作用就是为了帮助开发人员更好的完成项目,把大量复杂的实体和数据库的关系映射起来,并且实现了常用的CRUD和具有一定程序灵活性的查询方式。我现在是完全离不开ORM了,我理想中的数据访问层设计也是沿袭了ORM的使用方式。而NHibernate的首席开发人员Oren Eini还在他的一次NDC的演讲中认为不该为数据访问层花费任何时间,应该把时间放在和业务相关的代码上面,否则就是浪费金钱。

换句话说,使用ORM框架,其实就正好符合吉日“把项目做的又快又好又简单”的要求。的确,学习ORM需要成本,但也有俗话说“磨刀不误砍柴功”。如果说为了某个项目特地磨刀是浪费时间,加大风险,那么为什么不就在平时点滴时间内进行学习呢?如果你不学,那么你下个项目还是不会用,这样“要用再学”也永远只能是个梦想了。

至于代码生成器,的确好用,我也推荐多用。吉日在这里至少比手动硬写数据访问层的人聪明多了。他对于技术的态度,我还是不喜欢,但是可能的确可以反应一部分人的想法。

对于吉日文章的技术部分评价我就写到这里了,的确有不少问题,但也并非无一可取之处。看许多朋友和吉日的冲突,也大都是“意识形态”方面的问题吧。但既然是技术社区,那么我们至少也应该在技术角度上“率先”给予反馈——或反击吧?

嗯,我已经处理完技术方面的问题,那我也立即开始非技术方面的准备了(“吵架篇”在此)。