Страницы

Поиск по вопросам

пятница, 5 октября 2018 г.

Извлечение константы из Enum по ее текстовому значению

Имеется следующий метод, который возвращает именованную константу из enum по ее строковому представлению, либо возвращает значение по-умолчанию в случае неудачи:

static TEnum GetEnum(string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { TEnum result; return Enum.TryParse(key, ignoreCase, out result) ? result : defaultValue; }
Вопрос: почему в нижеприведенном коде для несуществующей константы возвращается некорректный результат? Как это исправить?

enum Nums { none = 0, one, two, three, four, five }
Nums res;
res = GetEnum("one"); Console.WriteLine($"{res} : {res == Nums.one}"); // one : True res = GetEnum("six"); Console.WriteLine($"{res} : {res == Nums.none}"); // none : True res = GetEnum("123"); Console.WriteLine($"{res} : {res == Nums.none}"); // 123 : False ???
Предполагаю, что так происходит из-за числовой интерпретации строки 123, но всё равно не понятно почему так происходит и как это исправить?


Ответ

Это странное поведение прописано в документации :-\
If value is a name that does not correspond to a named constant of TEnum, the method returns false. If value is the string representation of an integer that does not represent an underlying value of the TEnum enumeration, the method returns an enumeration member whose underlying value is value converted to an integral type. If this behavior is undesirable, call the IsDefined method to ensure that a particular string representation of an integer is actually a member of TEnum
Судя по всему, вам нужно переписать функцию в таком виде:
static TEnum GetEnum( string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { TEnum result; var b = Enum.TryParse(key, ignoreCase, out result) && Enum.IsDefined(typeof(TEnum), result); return b ? result : defaultValue; }

Обновление: Модифицированная функция не проходит тест со строками "1" и "one,one". Так что, судя по всему, нужно больше проверок:
static TEnum GetEnum( string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { TEnum result; var b = Enum.TryParse(key, ignoreCase, out result) && Enum.IsDefined(typeof(TEnum), result) && string.Equals(result.ToString(), key, StringComparison.OrdinalIgnoreCase); return b ? result : defaultValue; }

Обновление: Код, приведённый выше, не справляется с GetEnum("Error"), из-за того, что константы в System.Windows.Forms.MessageBoxIcon повторяются! В качестве решения, мы можем опросить список всех имён, и сравнить. Финальный вариант:
static TEnum GetEnum( string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { TEnum result; if (Enum.GetNames(typeof(TEnum)) .Contains(key, ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal) && Enum.TryParse(key, ignoreCase, out result)) return result; else return defaultValue; }
[Можно было бы обойтись проверкой Enum.IsDefined(typeof(TEnum), key) (не result!), но в ней невозможно запросить игнорирование регистра.]

Обновление: По результатам дискуссии в комментариях, можно воспользоваться менее красивым, но более эффективным кодом:
static TEnum GetEnum( string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { var comparisonType = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; var names = Enum.GetNames(typeof(TEnum)); foreach (var name in names) { if (name.Equals(key, comparisonType)) return (TEnum)Enum.Parse(typeof(TEnum), key, ignoreCase); } return defaultValue; }
Его суть та же, просто некоторые операции «расписаны». Бенчмарк в комментариях.

Если вам нужна экстремальная микрооптимизация, и вы понимаете проблемы с кешированием, вот вам ещё более быстрый кэширующий вариант, котрый придумал @Pavel Mayorov:
private class Holder { // относительный порядок элементов в массивах гарантирован документацией public static readonly TEnum[] values = (TEnum[])Enum.GetValues(typeof(TEnum)); public static readonly string[] names = Enum.GetNames(typeof(TEnum)); }
static TEnum GetEnum( string key, TEnum defaultValue = default(TEnum), bool ignoreCase = true) where TEnum : struct, IConvertible { var comparisonType = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; for (int i = 0; i < Holder.names.Length; i++) { if (Holder.names[i].Equals(key, comparisonType)) return Holder.values[i]; } return defaultValue; }

Комментариев нет:

Отправить комментарий