[Journal - Enums and Generics]

Enums and Generics

Thursday, February 16, 2006

Recall that we cannot constrain generic parameters to enumerations. In other words, we can't use System.Enum as a type constraint. The best we can do is use the value type constraint (where T : struct), as well as further constrain T to the interfaces we know every enumerated type happens to implement.

Enums aren't the only case where special kinds of constraints are needed but missing. These cases usually involve value types. What if you have a routine that requires integral numbers?

When I need to constrain a generic type parameter to an enumerated type, I've settled on the following:

where T : struct, IConvertible

Why do I need enum contraints anyway? Well, I'd like to provide certain utilities that perform bitwise operations on enumerated types in a type-safe manner. Suppose I want to Wave That Flag, uhm, walk the flags:

public static void WalkFlags(ulong value, Callback<bool, ulong> cb)
{
    ulong flag = 1;
    for(int i = 0; i < sizeof(ulong) * 8; i++)
    {
        if((flag & value) == flag)
        {
            if(false == cb(flag))
            {
                break;
            }
        }
        flag <<= 1;
    }
}

All these little operators, while available for enums, won't work on the generic type parameter T. So what I do in this case is to convert the enum to a ulong, walking the flags on that value:

public static void WalkFlags<T>(T value, Callback<bool, T> cb)
    where T : struct, IConvertible
{
    ulong val = Convert.ToUInt64(value);
    ulong flag = 1;
    for(int i = 0; i < sizeof(ulong) * 8; i++)
    {
        if((flag & val) == flag)
        {
            // ...
        }
        flag <<= 1;
    }
}

Of course, the callback should receive the flags as the enumerated type, so we instantiate the enumerated type (code below). More precisely, we create an instance initialized to the default value of the enum type. Why is that?

So the only way to change to value is to reflectively manipulate the value__ field (which is called fi):

object o = default(T); // keep reference to boxed enum value
object f = Convert.ChangeType(flag, fi.FieldType);
fi.SetValue(o, f);
T t = (T) o; // now, we can unbox

Note that we have to keep a reference to the boxed enum value arround, which we'll pass to FieldInfo.SetValue(). We cannot avoid boxing if we use reflection: any reflection operation works on the box, so we better keep a receipt. Also note that we change the flag value to the field type first, which is the enum's underlying type. The whole thing is wrapped up in Bytes.InstantiateEnum().

It seems silly to use reflection for these things, doesn't it:

So yes, I really want to have that T : enum constraint!

Enum-Generic Routines

An example for the flag walk is counting enumerator flags that are set. But in this case, we might as well convert to a ulong first:

public static int GetFlagCount<T>(T value)
    where T : struct, IConvertible
{
    ulong val = Convert.ToUInt64(value);
    return GetFlagCount(val);
}

public static int GetFlagCount(ulong val)
{
    int count = 0;
    Bytes.WalkFlags(
        val,
        delegate{count++; return true;}
    );
    return count;
}

So here's a list of routines in the Gregor.Core library where enumerations are used as generic type parameters: