如何在C#中模擬C++的聯合(Union)?[C#, C++]
How To Simulate C++ Union In C#?
Updated on Sunday, December 26, 2004
Written by Allen Lee
0 如何閱讀本文?
如果你...
- ...希望了解聯合的概念,請閱讀“什么是聯合?”。
- ...希望了解聯合的內存使用情況,請閱讀“聯合的內存布局與內存使用情況。”。
- ...希望了解如何在C#中模擬聯合,請閱讀“第一次嘗試:在C#中模擬這種布局方式。”。
- ...希望了解在C++中使用聯合有哪些要注意的地方,請閱讀“在實際的C++代碼中,我們是如何使用聯合的?”。
- ...希望了解如何在C#中更好的使用模擬的聯合,請閱讀“第二次嘗試:改進型的聯合模擬。”。
- ...希望了解在C#中使用模擬的聯合有些什么注意事項,請閱讀“別在模擬的聯合中同時使用值類型和引用類型!”。
- ...希望了解為何我要寫這篇文章,請閱讀“為什么要在C#里面模擬這個用處不大的東西?”。
否則...
- ...你應該從頭到尾閱讀全文。
1 什么是聯合?
聯合(Union)是一種特殊的類,一個聯合中的數據成員在內存中的存儲是互相重疊的。每個數據成員都在相同的內存地址開始。分配給聯合的存儲區數量是“要包含它最大的數據成員”所需的內存數。同一時刻只有一個成員可以被賦給一個值。
下面我們來看看C++中如何表達聯合:







2 聯合的內存布局與內存使用情況。
下面我們來考察一下TokenValue的內存布局。
首先,我們使用sizeof運算符來獲取該聯合各個成員的內存占用字節數:

















這樣,分配給該聯合的內存就是8個字節。
接著,我們來看看具體使用該聯合時,所分配的內存的字節占用情況如何:




















3 第一次嘗試:在C#中模擬這種布局方式。
在C#中,要指定成員的內存布局情況,我們需要結合使用StructLayoutAttribute特性、LayoutKind枚舉和FieldOffsetAttribute特性,它們都位于System.Runtime.InteropServices命名空間中。
下面我用struct來試著模擬上面的TokenValue聯合:













我們知道,聯合的每個數據成員都在相同的內存地址開始,通過把[FieldOffset(0)]應用到TokenValue的每一個成員,我們就指定了這些成員都處于同一起始位置。當然,我們得事先告訴.NET這些成員的內存布局由我們來作主,把LayoutKind.Explicit枚舉傳遞給StructLayoutAttribute特性的構造函數,并應用到TokenValue,.NET就不會再干涉該struct的成員在內存中的布局了。另外,我顯式的把TokenValue的大小設置為8字節,當然,這樣做是可選的。
4 在實際的C++代碼中,我們是如何使用聯合的?
在實際的C++代碼中,我們應盡量避免讓客戶端直接使用聯合,Code #03就是一個很好的反面例子了。為什么呢?熟悉C/C++的開發人員都知道,聯合提供我們這樣一個節省空間的儲存方式,是要我們付出一定的代價的。這個代價就是代碼的安全性,不恰當地使用聯合可能會導致程序崩潰的。
由于每一次只有一個聯合成員處于激活狀態,如果我們不小心或者因為其它原因使用處于休眠狀態的成員,輕則得到錯誤的結果,重則整個程序中止。請看下面的代碼:




















這里的TokenValue比起Code #01的僅僅多了一個_sval,它是C風格的字符串,實質上,它是指向字符串的第一個字符的指針,它占用4字節的內存空間。
當程序運行到Line #04時,就會出現Unhandled Exception,程序中止,并指出_sval的值非法(即所謂的“野指針”)。程序無法把它的值輸出控制臺,然而,Line #01 ~ Line #03都能輸出,只是Line #02和Line #03所輸出的值是錯誤的而已。
實際的應用中,我們一般不會看到如此低級且顯而易見的錯誤,但復雜的實際應用中,不恰當地使用聯合的確會為我們帶來不少的麻煩。
5 第二次嘗試:改進型的聯合模擬。
一般情況下,聯合作為一種內部數據的儲存手段,沒有必要讓客戶端對其有所了解,更沒必要讓客戶端直接使用它。為了使我們的聯合模擬用起來更安全,我們需要對它進行一番包裝:






























































































由于Token是值類型,實例化時,對應的成員(tv和tk)會自動被賦予與之對應的零值。此時,tv._cval為'\0'、tv._ival和tv._dval均為0(實質上它們是同一個值在不同的類型中的表現)。而tk也被自動賦予0:
tk = 0;
這里,你無需進行強類型轉換,0是任何枚舉的默認初始值,.NET會負責把0轉換成對應的枚舉類型。例如,你可以:



該代碼能正確輸出Sunday——一個星期的第一天(西方習慣),也是該枚舉的第一個成員。
一般情況下,0對應著枚舉的第一個成員(除非你在定義枚舉的時候,把第一個成員指定為別的值,并為別的成員賦予0值)。這樣,我們就不難看出代碼的輸出是合理的,而且代碼本身也是安全的。
6 別在模擬的聯合中同時使用值類型和引用類型!
到目前為止,我們所模擬的聯合中,所有的成員都是值類型,如果我們為它加入一個引用類型,例如String呢?
















這樣,Code #06的代碼運行時就會提示出錯:
TokenValue初始化的時候,_cval、_ival和_dval都能正確的被賦予對應的零值,而這些零值也能被統一起來(別的值就不行了)。但_sval不同,它是引用類型,如果沒有顯示初始化為某個有意義的值,它將被賦予null值!這個null值跟之前的有意義的零值是不能被統一起來的!所以,要么你就去掉這個_sval,要么就重新定義它的起始位置(當然,你也得去掉Size=8!),但這樣一來,TokenValue就不再稱得上聯合的模擬了。
在C++中,我們可以直接使用指針來解決這個問題,如Code #05,但C#中,問題就會變得有點辣手。如果你有興趣的話,可以使用不安全代碼(Unsafe code)來試著解決,但這樣一來,你的代碼又會引入一些新的問題。
7 為什么要在C#里面模擬這個用處不大的東西? [NEW]
相信很多人都有這樣一個疑問:為什么要在C#里面模擬這個用處不大的東西?就我個人來說,我始終堅信事物的存在必定有它的理由,否則就不會存在。其實,聯合在我們平時的編碼中的確很少用到,但在某些情況下,我們必須使用它!.NET為我們提供巨大的便利的同時,也不忘讓我們能夠與非托管代碼交互。你知道,早期的Win32 API使用C來完成的,這里面就有很多函數的參數是以聯合的形式表達的,要在C#中跟這些API交互,我們就得“尊重”原函數的用法約束。
8 終點與起點的交界處。
回顧整個探索旅程,我們為了使用聯合節省空間的優勢,開始了這個模擬的探索,然而,為了彌補聯合的不足,我們對這個模擬進行了一番包裝,增加了不少額外的代碼,直到后來,又發現了在這個模擬中同時使用值類型的成員和引用類型的成員所引發的問題,我們一直都沒有停止過探索和思考。正如馬斯洛的需要層次理論所描述的,人只要低層次的需要被滿足,馬上就會轉向更高的需要層次,一級一級的,直到攀上最高峰為止。
關于在C#中模擬C++的聯合這個話題,我并沒有在本文中給予你一個完整的展示,相反,我為你展示的僅僅是一個探索的起點,希望為你帶來一絲靈感,讓你根據自己的實際情況來定制你的探索旅程。Have a good trip!
參考資料:
- Stanley B.Lippman,Josee Lajoie; 《C++ Primer中文版(第三版)》 ;潘愛民,張麗譯;中國電力出版社 2002
- Microsoft .NET Framework SDK Documentation,Microsoft Corp. 2004
- Allen Lee; 《關于枚舉的種種 [C#, IL, BCL] 》
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
