構造体で共用体を作る
(StructLayout属性とFieldOffset属性)

注意:
この文書は以前「.NETでいきまっしょい!」で公開していたものですが、公開以降メンテナンスされていません。 今や古い情報となった内容が記載されている場合があるのでご注意ください。

1.StructLayout属性とFieldOffset属性


 .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の符号無し整数型を用いていると言うことを視覚的に表すためであって、それ以外に深い意味はありません。
StructLayout属性とFieldOffset属性
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
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ってビッグエンディアンでしたっけ・・・実はハードウェアとかメモリ関連のことはいまいちなのでもしかしたら図は間違ってるかもしれません・・・間違ってたらご指摘下さい。)

2.共用体を作る


 このように、StructLayout属性で明示的に配置方法を指定することができ、FieldOffsetでメンバ変数のオフセット値を指定することが出きるとなれば、共用体を作る方法はおのずと浮かんでくるはずです。 つまり、共用体の仕組み・構造を思い浮かべて下さい。 共用体では一つ以上の同じまたは異なる型のメンバが、同じメモリ領域を共用します。 つまり、メンバ変数のオフセット値を全て同じにすれば、共用体と同じ構造の構造体を作ることができることになります。
構造体で共用体を再現する
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
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++コードを次に示します。
上記の結果と同様の結果を得るC++コード
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
#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;
}

3.共用体の有効な利用法


 これまでのサンプルで、メンバ変数のオフセット値を明示的に設定することで、構造体を共用体と同じように扱ってきました。 しかし、共用体は使う場面が限られていて、たとえ使った場合でも見つけにくいバグを生じてしまうおそれがあるということは、CLSに共用体という概念が存在しないことからもいえることではないかと思います。 しかし、適切な場面で使えば共用体的な概念は有効でバグを減らす可能性も秘めています。
 次のサンプルは.NET FrameworkのSystem.Drawing名前空間にも存在するColor構造体を独自に定義したものです。
共用体の有効な使用例
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
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構造体をプロパティによってプログラムしたサンプルを載せておきます。 同じ機能を実現する場合において、その違いがよくわかると思います。
プロパティで再現した例
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
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") );
        }
    }
}