構造体で共用体を作る
(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") );
}
}
}
|