.NET框架:為什么我們要盡量使用框架內建的功能,而不是重新發明
有很多人經常會持有這樣的疑問:為什么 .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.aspx、http://www.ibm.com/developerworks/cn/java/j-dcl.html、http://msdn.microsoft.com/en-us/library/ff650316.aspx、http://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 框架會為你解決好那些惱人的跨平臺兼容性問題。