Индексирующие массивы с перечислениями в С#
У меня много коллекций фиксированного размера чисел, к которым можно получить доступ к каждой записи с константой. Естественно, это указывает на массивы и перечисления:
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;
}
который может быть самым быстрым способом, поскольку вы можете напрямую обращаться к массиву.
Ответы
Ответ 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?