tags:

views:

1307

answers:

6

I have a lot of fixed-size collections of numbers where each entry can be accessed with a constant. Naturally this seems to point to arrays and enums:

enum StatType {
    Foo = 0,
    Bar
    // ...
}

float[] stats = new float[...];
stats[StatType.Foo] = 1.23f;

The problem with this is of course that you cannot use an enum to index an array without a cast (though the compiled IL is using plain ints). So you have to write this all over the place:

stats[(int)StatType.foo] = 1.23f;

I have tried to find ways to use the same easy syntax without casting but haven't found a perfect solution yet. Using a dictionary seems to be out of the question since I found it to be around 320 times slower than an array. I also tried to write a generic class for an array with enums as index:

public sealed class EnumArray<T>
{
    private T[] array;
    public EnumArray(int size)
    {
        array = new T[size];
    }
    // slow!
    public T this[Enum idx]
    {
        get { return array[(int)(object)idx]; }
        set { array[(int)(object)idx] = value; }
    }
}

or even a variant with a second generic parameter specifying the enum. This comes quite close to what I want but the problem is that you cannot just cast an unspecific enum (be it from a generic parameter or the boxed type Enum) to int. Instead you have to first box it with a cast to object and then cast it back. This works, but is quite slow. I found that the generated IL for the indexer looks something like this:

.method public hidebysig specialname instance !T get_Item(!E idx) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: ldfld !0[] EnumArray`2<!T, !E>::array
    L_0006: ldarg.1 
    L_0007: box !E
    L_000c: unbox.any int32
    L_0011: ldelem.any !T
    L_0016: ret 
}

As you can see there are unnecessary box and unbox instructions there. If you strip them from the binary the code works just fine and is just a tad slower than pure array access.

Is there any way to easily overcome this problem? Or maybe even better ways? I think it would also be possible to tag such indexer methods with a custom attribute and strip those two instructions post-compile. What would be a suitable library for that? Maybe Mono.Cecil?

Of course there's always the possibility to drop enums and use constants like this:

static class StatType {
    public const int Foo = 0;
    public const int Bar = 1;
    public const int End = 2;
}

which may be the fastest way since you can directly access the array.

A: 

I don't believe there is any way to add an implicit conversion operator to an enum, unfortunately. So you'll have to either live with ugly typecasts or just use a static class with consts.

Here's a StackOverflow question that discusses more on the implicit conversion operator:

http://stackoverflow.com/questions/261663/can-we-define-implicit-conversions-of-enums-in-c

Steven Behnke
+5  A: 

I suspect you may be able to make it a bit faster by compiling a delegate to do the conversion for you, such that it doesn't require boxing and unboxing. An expression tree may well be the simplest way of doing that if you're using .NET 3.5. (You'd use that in your EnumArray example.)

Personally I'd be very tempted to use your const int solution. It's not like .NET provides enum value validation anyway by default - i.e. your callers could always cast int.MaxValue to your enum type, and you'd get an ArrayIndexException (or whatever). So, given the relative lack of protection / type safety you're already getting, the constant value answer is appealing.

Hopefully Marc Gravell will be along in a minute to flesh out the compiled conversion delegate idea though...

Jon Skeet
A: 

enums are supposed to be type safe. If you're using them as the index of an array, you're fixing both the type and the values of the enum, so you have no benefit over declaring a static class of int constants.

Michael Meadows
I guess it would be nice if you really could enforce type-safety on (some) enums at compile-time. Also for expressions like (enumValue + int) where int has a valid range which is known at compile-time.
gix
+4  A: 

If your EnumArray wasn't generic, but instead explicitly took a StatType indexer - then you'd be fine. If that's not desirable, then I'd probably use the const approach myself. However, a quick test with passing in a Func<T, E> shows no appreciable difference vs direct access.

 public class EnumArray<T, E> where E:struct {
    private T[] _array;
    private Func<E, int> _convert;

    public EnumArray(int size, Func<E, int> convert) {
        this._array = new T[size];
        this._convert = convert;
    }

    public T this[E index] {
        get { return this._array[this._convert(index)]; }
        set { this._array[this._convert(index)] = value; }
    }
 }
Mark Brackett
This seems like a nice workaround to cast a generic parameter. Though the generated IL contains a callvirt to the delegate and isn't optimized away during compilation or execution.
gix
+2  A: 

If you have a lot of fixed-size collections, then it would probably be easier to wrap up your properties in an object than a float[]:

public class Stats
{
    public float Foo = 1.23F;
    public float Bar = 3.14159F;
}

Passing an object around will give you the type safety, concise code, and constant-time access that you want.

And if you really need to use an array, its easy enough to add a ToArray() method which maps the properties of your object to a float[].

Juliet
While this might be good for simple things, it makes it difficult to loop over portions of the values. It also makes it a hassle to update them since there is a 1:N relation between arrays and enums, e.g. some enums are used to index several arrays.
gix
A: 

I'm not 100% familiar with C#, but I've seen implicit operators used to map one type to another before. Can you create an implicit operator for the Enum type that allows you to use it as an int?

Matthew Brubaker
You can only create these converters in either the source or the target class. So to do this you'd have to change either the `Int32` class or the class of the enum, which you can't do.
Konrad Rudolph
Didn't know that, thanks. =)
Matthew Brubaker