.NET Framework の共通言語仕様(CLS)には共用体は存在しません。 また、C#にもVB.NETにも共用体は存在しません。 共用体自体はそれほど使う機会が多いというわけではないですし、また、なければならないというものでもありません。 しかし、ごく稀にあると便利かなと思うことがあります。 そういうときに、.NET FrameworkのStructLayout属性とFieldOffset属性を用いることでC#やVB.NETの構造体でも共用体の機能を再現することができます。
それではまずStructLayout属性について見てみることにします。 この属性は構造体(クラスも可)に対して適用し、この属性を適用することで構造体の各メンバ変数のメモリ上での配置方法を指定することができます。 具体的には、LayoutKind.Autoを指定するとコンパイラが最も適した方法で配置し、LayoutKind.Sequentialを指定するとメモリ内に連続して順番に配置され、 LayoutKind.Explicitを指定するとプログラム側で明示的に位置を指定しなければなりません。
また、ここでLayoutKind.Explicitを指定すると、全てのメンバ変数に対して明示的に位置を指定しなければなりません。 そこで、位置を指定するために使用する属性がFieldOffset属性です。 この属性では構造体の先頭から各メンバ変数の先頭までのオフセット値をバイト単位で指定します。
それでは、早速これらの属性を適用した構造体を作成してみようと思います。 このサンプルではshortではなく System.UInt16を用いていますが、16Bitの符号無し整数型を用いていると言うことを視覚的に表すためであって、それ以外に深い意味はありません。
using System; using System.Runtime.InteropServices; namespace StructAndUnion { // StructLayout属性及びFieldOffset属性を適用した構造体 [StructLayout(LayoutKind.Explicit)] struct SampleStruct { [FieldOffset(0)] public System.UInt16 Value1; [FieldOffset(2)] public System.UInt16 Value2; [FieldOffset(4)] public System.UInt16 Value3; [FieldOffset(6)] public System.UInt16 Value4; } // アプリケーションのエントリーポイントを提供するクラス class SampleClass { [STAThread] static void Main(string[] args) { SampleStruct s = new SampleStruct(); // 各フィールドに値を指定 s.Value1 = 0x1122; s.Value2 = 0x3344; s.Value3 = 0x5566; s.Value4 = 0x7788; // 各フィールドの値を表示 Console.WriteLine( "SampleStruct.Value1 : 0x" + s.Value1.ToString("X4") ); Console.WriteLine( "SampleStruct.Value2 : 0x" + s.Value2.ToString("X4") ); Console.WriteLine( "SampleStruct.Value3 : 0x" + s.Value3.ToString("X4") ); Console.WriteLine( "SampleStruct.Value4 : 0x" + s.Value4.ToString("X4") ); } } }
SampleStruct.Value1 : 0x1122 SampleStruct.Value2 : 0x3344 SampleStruct.Value3 : 0x5566 SampleStruct.Value4 : 0x7788 Press any key to continue
この結果を見てわかるとおり、通常の構造体の場合と何ら変わりないように思われます。 しかし、メモリ上の配置は図のようになっているはずです。 (ところで、.NET Frameworkってビッグエンディアンでしたっけ・・・実はハードウェアとかメモリ関連のことはいまいちなのでもしかしたら図は間違ってるかもしれません・・・間違ってたらご指摘下さい。)
このように、StructLayout属性で明示的に配置方法を指定することができ、FieldOffsetでメンバ変数のオフセット値を指定することが出きるとなれば、共用体を作る方法はおのずと浮かんでくるはずです。 つまり、共用体の仕組み・構造を思い浮かべて下さい。 共用体では一つ以上の同じまたは異なる型のメンバが、同じメモリ領域を共用します。 つまり、メンバ変数のオフセット値を全て同じにすれば、共用体と同じ構造の構造体を作ることができることになります。
using System; using System.Runtime.InteropServices; namespace StructAndUnion { // 構造体で再現した共用体 [StructLayout(LayoutKind.Explicit)] struct DoubleWord { [FieldOffset(0)] public System.UInt32 Value; [FieldOffset(0)] public System.UInt16 LowWord; [FieldOffset(2)] public System.UInt16 HighWord; } // アプリケーションのエントリーポイントを提供するクラス class SampleClass { [STAThread] static void Main(string[] args) { DoubleWord dw = new DoubleWord(); // ダブルワード値を指定 dw.Value = 0x11223344; // 下位ワードと上位ワードを表示 Console.WriteLine( "Low word: 0x" + dw.LowWord.ToString("X4") ); Console.WriteLine( "High word: 0x" + dw.HighWord.ToString("X4") ); } } }
Low word : 0x3344 High word : 0x1122 Press any key to continue
この結果から、この構造体が共用体と同様の機能を得ていることがわかります。 上の図は、その概念図です。 これと同様の結果を得るためのC++コードを次に示します。
#include <iostream> using namespace std; struct DWord { unsigned short LowWord; unsigned short HighWord; }; union DoubleWord { unsigned long Value; DWord DWord; }; int main() { DoubleWord dw; dw.Value = 0x11223344; cout << "Low word: 0x" << hex << dw.DWord.LowWord << endl; cout << "High word: 0x" << hex << dw.DWord.HighWord << endl; return 0; }using namespace std; struct DWord { unsigned short LowWord; unsigned short HighWord; }; union DoubleWord { unsigned long Value; DWord DWord; }; int main() { DoubleWord dw; dw.Value = 0x11223344; cout << "Low word: 0x" << hex << dw.DWord.LowWord << endl; cout << "High word: 0x" << hex << dw.DWord.HighWord << endl; return 0; }]]>
これまでのサンプルで、メンバ変数のオフセット値を明示的に設定することで、構造体を共用体と同じように扱ってきました。 しかし、共用体は使う場面が限られていて、たとえ使った場合でも見つけにくいバグを生じてしまうおそれがあるということは、CLSに共用体という概念が存在しないことからもいえることではないかと思います。 しかし、適切な場面で使えば共用体的な概念は有効でバグを減らす可能性も秘めています。
次のサンプルは.NET FrameworkのSystem.Drawing名前空間にも存在するColor構造体を独自に定義したものです。
using System; using System.Runtime.InteropServices; namespace StructAndUnion { // 構造体で再現した共用体 [StructLayout(LayoutKind.Explicit)] struct Color { [FieldOffset(0)] public System.UInt32 Value; [FieldOffset(0)] public System.Byte R; [FieldOffset(1)] public System.Byte G; [FieldOffset(2)] public System.Byte B; [FieldOffset(3)] public System.Byte A; } // アプリケーションのエントリーポイントを提供するクラス class SampleClass { [STAThread] static void Main(string[] args) { Color c = new Color(); // 色の値を4バイトの整数で指定 c.Value = 0x80e0c0a0; // 各色要素の値表示を表示 Console.WriteLine( "Alpha: 0x" + c.A.ToString("X2") ); Console.WriteLine( "R: 0x" + c.R.ToString("X2") ); Console.WriteLine( "G: 0x" + c.G.ToString("X2") ); Console.WriteLine( "B: 0x" + c.B.ToString("X2") ); } } }
Alpha: 0x80 R: 0xA0 G: 0xC0 B: 0xE0 Press any key to continue
Color構造体はA・R・G・Bの四つの値を持つことができます。 また、この値は場合によっては4バイトの整数として取得または設定したいということもあります。 このように、一つの型でありながら、複数の表現方法があるときなどには共用体の概念は非常に有効です。
また、StructLayout属性とFieldOffset属性を使用すると、構造体によって共用体と同様の機能を得ると同時に、共用体以上の機能を持った構造体を作ることも可能です。 最後に、先ほどのColor構造体をプロパティによってプログラムしたサンプルを載せておきます。 同じ機能を実現する場合において、その違いがよくわかると思います。
using System; namespace StructAndUnion { struct Color { private System.UInt32 val; private System.Byte r; private System.Byte g; private System.Byte b; private System.Byte a; public System.UInt32 Value { get { return val; } set { val = value; r = (System.Byte)( val & 0xff ); g = (System.Byte)( ( val >> 8 ) & 0xff ); b = (System.Byte)( ( val >> 16 ) & 0xff ); a = (System.Byte)( ( val >> 24 ) & 0xff ); } } public System.Byte R { get { return r; } set { r = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte G { get { return g; } set { g = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte B { get { return b; } set { b = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte A { get { return a; } set { a = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } } // アプリケーションのエントリーポイントを提供するクラス class SampleClass { [STAThread] static void Main(string[] args) { Color c = new Color(); // 色の値を4バイトの整数で指定 c.Value = 0x80e0c0a0; // 各色要素の値表示を表示 Console.WriteLine( "Alpha: 0x" + c.A.ToString("X2") ); Console.WriteLine( "R: 0x" + c.R.ToString("X2") ); Console.WriteLine( "G: 0x" + c.G.ToString("X2") ); Console.WriteLine( "B: 0x" + c.B.ToString("X2") ); } } }> 8 ) & 0xff ); b = (System.Byte)( ( val >> 16 ) & 0xff ); a = (System.Byte)( ( val >> 24 ) & 0xff ); } } public System.Byte R { get { return r; } set { r = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte G { get { return g; } set { g = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte B { get { return b; } set { b = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } public System.Byte A { get { return a; } set { a = value; val = (System.UInt32)( a << 24 | b << 16 | g << 8 | r ); } } } // アプリケーションのエントリーポイントを提供するクラス class SampleClass { [STAThread] static void Main(string[] args) { Color c = new Color(); // 色の値を4バイトの整数で指定 c.Value = 0x80e0c0a0; // 各色要素の値表示を表示 Console.WriteLine( "Alpha: 0x" + c.A.ToString("X2") ); Console.WriteLine( "R: 0x" + c.R.ToString("X2") ); Console.WriteLine( "G: 0x" + c.G.ToString("X2") ); Console.WriteLine( "B: 0x" + c.B.ToString("X2") ); } } }]]>