代码改变世界

.NET框架:为什么我们要尽量使用框架内建的功能,而不是重新发明

2011-01-05 15:08  Nana's Lich  阅读(7306)  评论(20编辑  收藏  举报

有很多人经常会持有这样的疑问:为什么 .NET 框架要把一些很简单的功能也封装起来?

而有些人所坚持的“有现成的就用现成的”的习惯在那些“明明只是很简单的功能却被封装了起来”的情况下也显得很可笑。

 

那么,实际上到底有没有必要用那些本来就很简单的封装?这些简单的封装到底具有什么样的意义呢?

 

其实大部分这样的简单的封装都是针对“跨平台使用”而设计的。

有些人可能会说:.NET 框架有什么跨平台可言?

其实 .NET 框架虽然现在只提供 Windows 上的版本,但其它平台上的 CLI 实现,如 Mono、DotGNU 等等也都有赖于 .NET 框架和 CLI 的预见性方能成为现实;而可以在多种环境中使用的 RIA 平台 Silverlight 也是将这种思想发挥到了极致。

 

举个例子来说,.NET 框架中 IPAddress 类型具有 NetworkToHostOrder 和 HostToNetworkOrder 方法,如果你使用 Reflector 来查看反编译后的代码,你会发现 NetworkToHostOrder 只是调用了 HostToNetworkOrder,而 HostToNetworkOrder 的原理也只不过是一些简单的位移运算而已。

有的人看到这里可能会想:包了两层方法性能多差啊,用到它的地方自己写位移运算不是也可以么?

不要这样做。

 

实际上,CLR 的 JIT 编译功能会把简单的方法进行内联编译,所以像是 NetworkToHostOrder 这样的方法在进行 JIT 编译之后结果和直接使用位移运算并没有区别,而在这里偏执地直接使用位移运算,不仅性能没有实质上的提升,还会导致代码难以维护;而且这样的代码如果到了使用 Big-Endian 字节序的计算机上,就不能用了!

当然了,如果你善于使用预编译指令之类的工具,这种问题也自然难不倒你。

 

与此相似的,还有:

有些具有 Visual Basic 5/6 编程经验的人在使用 Win32API 的时候会习惯使用 Long 或者 Int32 来当作各种 Handle 的等价类型,然而这样做是错的!

如果你去查看 SDK 中关于 HANDLE 的定义,你会发现:

typedef PVOID HANDLE;

也就是说,实际上在 64 位程序里,Handle 和指针一样,是 64 位的——虽然绝大部分情况下这 64 位的 Handle 只有低 32 位有效,但是数据类型本身需要的 64 位空间不能忽视。

所以实际上在需要用到任何类型的 Handle 的场所,都应该使用 IntPtr,而非 Visual Basic 中的 Long 或者 CLR 中的 Int32,这样才能保证代码在 IA-64、x86-64 硬件平台上正常运行。

 

而最为有用,又最为复杂,以致于很多人不愿意去了解的,则是关于“低锁定技术”的各种封装。

例如,ReaderWriterLock(Slim)、Lazy<T>、ConcurrentDictionary<TKey, TValue>等等。

由于这些封装的内部实现实在是很复杂,不管是从篇幅上考虑还是从笔者我的能力上去考虑,想详细、深入地去解释实在是不现实,所以这里只能介绍一下大致上的原理了。

 

低锁定技术是一种由来已久的设计方式,有兴趣的人可以在网络上自行搜索相关的资料。

低锁定技术的大致思路就是:对于没有初始化的值,在第一次读取的时候需要锁定并将之初始化;一经初始化之后,进行读取时不需要任何锁定操作。

不过尽管低锁定看上去是个很聪明的做法,可实际实现起来却要考虑很多问题,因此也有很多文章警告过,不要尝试实现低锁定,因为实际上是会产生意外的。

实际上 CPU 可能会按照一定的模式来优化内存操作,改变某些读、写操作的顺序;又或者是因为读取了已经缓存的数据而导致了判断失误,致使同一时间尝试进行访问的其它线程读取到了错误的数据,结果采用低锁定技术的程序却发生了异常。

关于这个问题,想要了解细节的同学可以参考这些文章:http://msdn.microsoft.com/en-us/magazine/cc163715.aspxhttp://www.ibm.com/developerworks/cn/java/j-dcl.htmlhttp://msdn.microsoft.com/en-us/library/ff650316.aspxhttp://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

 

尽管这样的问题也可以通过精密的设计而解决,但如果你使用过 C/C++ 并且知道 MemoryBarrier 的概念,你也可以发现下面这些东西:

#ifdef _AMD64_
#define MemoryBarrier __faststorefence
#endif

#ifdef _IA64_
#define MemoryBarrier __mf
#endif

// x86

FORCEINLINE
VOID
MemoryBarrier (
    VOID
    )
{
    LONG Barrier;
    __asm {
        xchg Barrier, eax
    }
}

这也说明在不同的平台上,由内存模型或其它技术细节上的差异所导致的问题,具体的解决办法也是各不相同的。

而对于我们一般的应用程序开发人员来说,要想全面掌握这些解决办法,并且在每个必要的场合准确无误地采取合适的办法也是相当困难的。

 

其实这样繁琐的问题本来就不应该让一般应用程序开发人员去面对,而是应该让 .NET 框架之类的靠近底层的设施去解决。

所以对于使用 C/C++ 的开发人员来说,当他们用到 MemoryBarrier 的时候,并不需要真的根据目标平台而写入实际的操作代码,只需要插入相应的 MACRO 就可以了。

而实际上,.NET 框架也把这一部分隐藏了起来,在 MSDN 博客上有博文解释了在 CLR 中实现了一种正确的内存操作模型(链接:http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx),而且无论哪种平台上的 .NET 框架(或者其等价物)都会保证它们的行为一致;

同时 .NET 框架中提供的诸如 Lazy<T>、ConcurrentDictionary<TKey, TValue> 等类型,也有助于避免由于对内存模型不了解(比方说,你知道使用 lock 时是不是应该同时使用 MemoryBarrier 或者 Volatile 吗?又或者,你知道应该在什么时候使用、什么时候不需要使用吗?),或者一时间的逻辑误区而写出了存在潜在问题、导致程序不稳定的代码的情形发生。

 

总地来说,除非是对底层的技术细节具有相当高的程度的了解,并且具有极强的信心可以解决任何问题……否则还是“有现成的就用现成的”比较好,因为这样做的话 .NET 框架会为你解决好那些恼人的跨平台兼容性问题。