17.6 Simulating a C Union
Each field in a struct is
given enough
room to store its data. Consider a struct
containing one int and one
char. The int is likely to
start at an offset of 0, and is guaranteed at least four bytes. So,
the char could start at an offset of 4. If, for some reason, the
char started at an offset of 2, then
you'd change the value of the int
if you assigned a value to the char. Sounds like
mayhem, doesn't it? Strangely enough, the C language
supports a variation on a struct called a union
that does exactly this. You can simulate this in C# using
LayoutKind.Explicit and the
FieldOffset attribute.
It might be hard to think of a case in which this would be useful.
However, consider a situation in which you want to create a vast
quantity of different primitive types, but store them in one array.
You have to store them all in an object array, and the primitive
values are boxed every time they go into the array. This means
you're going to start using a lot of heap memory,
and you'll pay the cost of boxing and unboxing as
you go from primitive to object and back again. This example shows
how this works with three objects, but if it were thousands or
millions of objects, the impact on performance would be noticeable:
using System;
public class BoxMe {
public static void Main( ) {
// Stuff some primitive values into an array.
object[ ] a = new object[3];
a[0] = 1073741824;
a[1] = 'A';
a[2] = true;
// Display each value
foreach (object o in a) {
Console.WriteLine("Value: {0}", o);
}
}
}
To avoid the boxing and unboxing operations, you can use a union to
create a variant type, which is a
type that can take on one value at a
time, but can represent a variety of types. Your variant type has a
flag field that tells you the type that the variant represents. It
also includes one value field for each of the possible types it can
take on. Since each value field starts at the same offset, a variant
instance never takes up more memory than the largest type plus the
size of the flag field (it may also take a couple of bytes to
optimally align the fields in memory).
The next example shows the use of a variant struct called
MyVariant. It can be an int, char, or bool. The
value fields intVal, charVal,
and boolVal are stored at an offset
(sizeof(byte)) that sets aside enough memory for
the flag field vType. This means that the value
fields are overlapped. Since the variant can only represent one value
at any given time, initializing charVal as
"A" will produce that same value
even if you inspect intVal. However, the value is
treated as an int giving 65, the ASCII value of
"A", as shown in this example:
using System;
using System.Runtime.InteropServices;
// Enumerate the possible types for MyVariant
public enum MyVariantType : byte { Int, Char, Bool };
// Define a structure for MyVariant
[StructLayout(LayoutKind.Explicit)]
public struct MyVariant {
// Type flag
[FieldOffset (0)] public MyVariantType vType;
// Start the fields, leaving enough room for vType
[FieldOffset (sizeof(byte))] public int intVal;
[FieldOffset (sizeof(byte))] public char charVal;
[FieldOffset (sizeof(byte))] public bool boolVal;
// Return a string representation of this Variant
public override string ToString( ) {
switch (vType) {
case MyVariantType.Int:
return intVal.ToString( );
case MyVariantType.Char:
return charVal.ToString( );
case MyVariantType.Bool:
return boolVal.ToString( );
}
throw new Exception("Unknown Variant type: " + vType);
}
}
// Create an array of variants and display their values
public class VariantTest {
public static void Main( ) {
MyVariant[ ] a = new MyVariant[3];
a[0].vType = MyVariantType.Int;
a[0].intVal = 1073741824;
a[1].vType = MyVariantType.Char;
a[1].charVal = 'A';
a[2].vType = MyVariantType.Bool;
a[2].boolVal = true;
// Display each variant's value
foreach (MyVariant mv in a) {
Console.WriteLine("Value: {0}", mv);
}
// Reinterpret the char as an int
Console.WriteLine("{0} is: {1}", a[1], a[1].intVal);
}
}
|