使用C#中的枚举类型索引数组

20

我有很多固定大小的数字集合,其中每个条目都可以通过常量访问。自然而然地,这似乎指向数组和枚举:

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

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

当然,这样做的问题是你不能使用枚举来索引数组,除非进行类型转换(尽管编译后的IL使用普通整数)。因此,你必须在许多地方重复编写以下代码:

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

我曾尝试找到不需要强制转换的简单语法,但是还没有找到完美的解决方案。使用字典似乎是行不通的,因为我发现它比数组慢了约320倍。我还尝试编写一个通用类来将枚举作为索引的数组:

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; }
    }
}

甚至可以使用第二个泛型参数来指定枚举类型。这很接近我想要的,但问题是您不能将未指定的枚举(无论是来自泛型参数还是装箱类型Enum)直接转换为int。相反,您必须先使用对象强制转换进行装箱,然后再进行强制转换。这样做虽然可行,但速度较慢。我发现索引器生成的IL大致如下:

.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 
}

正如你所看到的,这里有一些不必要的盒子(box)和取消盒子(unbox)指令。如果你从二进制代码中去掉它们,那么代码就可以很好地运行,并且只比纯数组访问略慢一点。

有没有什么简单的方法来解决这个问题?或者说甚至有更好的方法吗? 我认为也可以使用自定义属性标记此类索引器方法,并在编译后删除这两个指令。哪个库比较适用?也许是Mono.Cecil?

当然,总是有可能放弃枚举并像这样使用常量:

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

这可能是最快的方法,因为您可以直接访问数组。

7个回答

8
我猜你可以通过编译一个委托来加快转换速度,这样就不需要装箱和拆箱了。如果你使用的是.NET 3.5,那么表达式树可能是最简单的方法。(在EnumArray示例中使用它。)
个人而言,我很想使用你的const int解决方案。毕竟,.NET默认情况下并没有提供枚举值验证——也就是说,你的调用者总是可以将int.MaxValue强制转换为你的枚举类型,然后你会得到一个ArrayIndexException(或其他异常)。因此,考虑到你已经相对缺乏保护/类型安全性,所以常量值的答案很有吸引力。
希望Marc Gravell能在一分钟内详细说明编译转换委托的想法……

5
如果你的EnumArray不是泛型的,而是明确地采用了StatType索引器-那么你会没问题。如果这不是你想要的,那么我可能会自己使用const方法。然而,通过传递一个Func<T,E>进行快速测试,显示与直接访问没有显着的差异。
 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; }
    }
 }

这似乎是一种很好的解决方法,可用于转换泛型参数。虽然生成的 IL 包含对委托的 callvirt 调用,并且在编译或执行期间不会被优化掉。 - gix

3

如果您有许多固定大小的集合,那么将属性封装在对象中可能比float[]更容易:

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

传递对象会给你想要的类型安全性,简洁的代码和恒定时间访问。

如果你真的需要使用数组,可以很容易地添加一个ToArray()方法,将对象的属性映射到float[]中。


虽然这对于简单的事情可能是好的,但它使得循环遍历值的部分变得困难。由于数组和枚举之间存在1:N关系,例如一些枚举用于索引多个数组,因此更新它们也会变得麻烦。 - gix

3
struct PseudoEnum 
{ 
    public const int INPT = 0; 
    public const int CTXT = 1; 
    public const int OUTP = 2; 
}; 

// ... 

String[] arr = new String[3]; 

arr[PseudoEnum.CTXT] = "can"; 
arr[PseudoEnum.INPT] = "use"; 
arr[PseudoEnum.CTXT] = "as"; 
arr[PseudoEnum.CTXT] = "array"; 
arr[PseudoEnum.OUTP] = "index"; 

(我也在https://dev59.com/-3VC5IYBdhLWcg3wihqv#12901745发布了这个答案)

[编辑:哎呀,我刚刚注意到Steven Behnke在本页面的其他地方提到了这种方法。很抱歉;但至少这展示了一个执行它的例子...]


1

枚举类型应该是类型安全的。如果你将它们用作数组的索引,那么你固定了枚举的类型和值,因此与声明一个包含 int 常量的静态类相比没有任何好处。


我想如果你真的可以在编译时对(某些)枚举强制执行类型安全,那就太好了。还有像(enumValue + int)这样的表达式,其中int具有已知的编译时有效范围。 - gix

0
很遗憾,我不认为有任何方法可以为枚举添加隐式转换运算符。因此,你必须要么忍受难看的类型转换,要么只使用一个包含常量的静态类。
下面是一个StackOverflow问题,它讨论了更多关于隐式转换运算符的内容: C#中是否可以定义枚举的隐式转换?

0

我对C#不是百分之百熟悉,但我之前见过使用隐式转换操作符将一种类型映射到另一种类型。你能为枚举类型创建一个隐式转换操作符,使其可以像int一样使用吗?


你只能在源类或目标类中创建这些转换器。因此,要做到这一点,您必须更改Int32类或枚举类的类,而这是不可能的。 - Konrad Rudolph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接