Подтвердить что ты не робот

Индексирующие массивы с перечислениями в С#

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

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

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

Проблема с этим, конечно, в том, что вы не можете использовать перечисление для индексации массива без приведения (хотя скомпилированный IL использует простые ints). Поэтому вы должны написать это повсюду:

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

или даже вариант со вторым общим параметром, определяющим перечисление. Это очень близко к тому, что я хочу, но проблема заключается в том, что вы не можете просто указать неспецифическое перечисление (будь то из общего параметра или Enable в штучной упаковке) в 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 
}

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

Ответ 2

Если ваш 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; }
    }
 }

Ответ 3

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

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

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

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

Ответ 4

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

(Я также разместил этот ответ на fooobar.com/questions/54788/...)

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

Ответ 5

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

Ответ 6

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

Здесь задается вопрос StackOverflow, в котором больше обсуждается оператор неявного преобразования:

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

Ответ 7

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