In a certain context I need to manage constrained values. To simplify; let's just say that I need to constrain values to either be a string or a 64-bit integer.
For this purpose; I'm considering declaring a struct type that has one field for the type of value being stored and one field for the actual value.
In this simplified case, the type field could perhaps be omitted because we could distinguish string and integer by their CLR type. However; I need the type field for other purposes (more than one "constrained value type" can be represented by a single CLR type).
A straight forward approach:
public struct MyValue
{
private object _value;
private MyValueType _type;
public string String
{
get
{
// todo: check type
return (string)_value;
}
set
{
// todo: validate value
_type = MyValueType.String;
_value = value;
}
}
public long Int64
{
get
{
// todo: check type
return (long)_value;
}
set
{
// todo: validate value
_type = MyValueType.Int64;
_value = value;
}
}
}
However, this approach requires some "extra" IL instructions:
castclass
instruction to cast from object to string.unbox.any
instruction to cast from object to long.box
instruction to cast from long to object.The purpose of this struct is to enforce constraints, so by the time its getting or setting a value it is known that it'll be of the right type.
Therefore I'm considering using FieldOffset attributes. Something like this:
[StructLayout(LayoutKind.Explicit)]
public struct MyValue
{
[FieldOffset(0)]
private string _string;
[FieldOffset(0)]
private long _int64;
[FieldOffset(8)]
private MyValueType _type;
public string String
{
get
{
// todo: check type
return _string;
}
set
{
// todo: validate value
_type = MyValueType.String;
_string = value;
}
}
public long Int64
{
get
{
// todo: check type
return _int64;
}
set
{
// todo: validate value
_type = MyValueType.Int64;
_int64 = value;
}
}
}
With this approach there are no extra box, unbox, or cast instructions. Which makes me think that this approach is better.
Question is: Are there any drawbacks to using explict struct layout and field offset attributes?
Perhaps there the JIT-compiler would choke on this for some reason?
In real code; the struct would be immutable. The fields would be read-only and it would not have any setters.
First, I didn't think this would make a difference, since it would just more or less mean that the setters would move into constructors, one for each value type.
However; the compiler requires that all struct members are initialized by a constructor – without regard that they have the same field offset.
I need to do something like this:
public MyValue(string value)
{
// todo: validate value
_int64 = 0; // just to satisfy the compiler
_string = value;
_type = MyValueType.String;
}
public MyValue(long value)
{
// todo: validate value
_string = null; // just to satisfy the compiler
_int64 = value;
_type = MyValueType.Int64;
}
This means that the second approach require "extra" IL instructions too. There are three extra instructions for "defaulting" each field that won't be used.
For example: Setting _string = null
yield ldarg.0
, ldnull
and stfld
.
These extra instructions are completely wasteful. And it would get worse if I would add additional fields.
So; question is also: Will the JIT-compiler be smart enough to disregard these wasteful instructions?
Nevermind. It seems that this can't be done anyway. Trying to load such a type will cause a TypeLoadException
saying something like
Could not load type 'MyValue' from assembly 'MyAssembly' because it contains an object field at offset 0 that is incorrectly aligned or overlapped by a non-object field.
So, I guess it is not possible to have a reference type field on the same offset as a value type field.
End of story.
I'm leaving this Q&A (not deleting) though in case anyone else is curious.