Индексирование массивов с перечислениями в C # - PullRequest
17 голосов
/ 14 января 2009

У меня есть много наборов чисел фиксированного размера, где каждая запись может быть доступна с константой. Естественно, это указывает на массивы и перечисления:

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

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

Проблема с этим, конечно, в том, что вы не можете использовать enum для индексации массива без преобразования (хотя скомпилированный 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 
}

Как видите, там есть ненужные инструкции для ящиков и распаковщиков. Если вы удалите их из двоичного файла, код будет работать нормально и чуть медленнее, чем чистый доступ к массиву.

Есть ли способ легко преодолеть эту проблему? Или, может быть, даже лучшие способы? Я думаю, что было бы также возможно пометить такие методы индексатора пользовательским атрибутом и удалить эти две инструкции после компиляции. Что будет подходящей библиотекой для этого? Может быть, Mono.Cecil?

Конечно, всегда есть возможность отбросить перечисления и использовать такие константы, как это:

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

, что может быть самым быстрым способом, поскольку вы можете напрямую обращаться к массиву.

Ответы [ 7 ]

8 голосов
/ 14 января 2009

Я подозреваю, что может быть в состоянии сделать это немного быстрее, скомпилировав делегата, который сделает преобразование для вас, так, чтобы он не требовал упаковки и распаковки. Дерево выражений может быть самым простым способом сделать это, если вы используете .NET 3.5. (Вы использовали бы это в своем примере EnumArray.)

Лично мне бы очень хотелось использовать ваше решение const int. Это не значит, что .NET в любом случае обеспечивает проверку значения перечисления по умолчанию - то есть ваши вызывающие всегда могут привести int.MaxValue к вашему типу перечисления, и вы получите ArrayIndexException (или что-то еще). Таким образом, учитывая относительную нехватку защиты / безопасности типов, которую вы уже получаете, ответ с постоянным значением является привлекательным.

Надеюсь, что через минуту Марк Гравелл придет, чтобы раскрыть идею скомпилированного делегата преобразования ...

5 голосов
/ 14 января 2009

Если ваш EnumArray не является универсальным, а вместо этого явно использует индексатор StatType - тогда все будет в порядке. Если это не желательно, то я, вероятно, сам использовал бы подход const. Однако быстрый тест с передачей в Func не показывает заметной разницы по сравнению с прямым доступом.

 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; }
    }
 }
3 голосов
/ 15 октября 2012
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://stackoverflow.com/a/12901745/147511)

[edit: упс. Я только что заметил, что Стивен Бенке упоминал этот подход в другом месте на этой странице. Сожалею; но, по крайней мере, это показывает пример того, как это делается ...]

3 голосов
/ 14 января 2009

Если у вас много коллекций фиксированного размера, то, вероятно, будет проще обернуть ваши свойства в объекте, чем float []:

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

Передача объекта даст вам безопасность типов, краткий код и постоянный доступ, который вам нужен.

И если вам действительно нужно использовать массив, достаточно просто добавить метод ToArray (), который отображает свойства вашего объекта в float [].

1 голос
/ 14 января 2009

перечисления должны быть типобезопасными. Если вы используете их как индекс массива, вы фиксируете как тип, так и значения перечисления, поэтому у вас нет никакой выгоды по сравнению с объявлением статического класса int-констант.

0 голосов
/ 14 января 2009

Я не на 100% знаком с C #, но я видел неявные операторы, которые раньше отображали один тип на другой. Можете ли вы создать неявный оператор для типа Enum, который позволит вам использовать его как int?

0 голосов
/ 14 января 2009

Я не верю, что есть какой-либо способ добавить оператор неявного преобразования в перечисление, к сожалению. Так что вам придется либо жить с ужасными типами, либо просто использовать статический класс с consts.

Вот вопрос StackOverflow, в котором более подробно рассматривается оператор неявного преобразования:

Можем ли мы определить неявные преобразования перечислений в c #?

...