Путаница с разбором Enum

Я преобразовываю числовое значение, которое имеет тип строки в соответствующий Enum. Пока я тестировал свой код, я обнаружил интересное поведение, которое меня смутило.

Используя приведенный ниже пример кода, может ли кто-нибудь пролить свет на то, почему исключение не возникает, если/когда переменная s имеет значение, которое не соответствует одному из значений Enum? Кроме того, как можно установить значение sEnum var в значение, которое не существует в определении перечисления Stooge?

class Program
{
    enum Stooge
    {
        Unspecified,
        Moe,
        Larry,
        Curly,
        Shemp
    }

    static void Main(string[] args)
    {
        while (true)
        {
            Console.WriteLine("Enter a number...");

            string s = Console.ReadLine();
            Stooge sEnum = (Stooge)(int.Parse(s)); //Why doesn't this line throw if s != 0, 1, 2, 3, or 4?

            Console.WriteLine("\r\nYou entered: {0}\r\nEnum String Value: {1}\r\nEnum Int Value: {2}\r\n", s, sEnum.ToString(), (int)sEnum);
        }
    }
}

Ответ 1

Это было решение со стороны людей, которые создали .NET. Перечисление поддерживается другим типом значения (int, short, byte и т.д.), Поэтому оно может иметь любое значение, допустимое для этих типов значений.

Лично я не фанат того, как это работает, поэтому я сделал ряд полезных методов:

/// <summary>
/// Utility methods for enum values. This static type will fail to initialize 
/// (throwing a <see cref="TypeInitializationException"/>) if
/// you try to provide a value that is not an enum.
/// </summary>
/// <typeparam name="T">An enum type. </typeparam>
public static class EnumUtil<T>
    where T : struct, IConvertible // Try to get as much of a static check as we can.
{
    // The .NET framework doesn't provide a compile-checked
    // way to ensure that a type is an enum, so we have to check when the type
    // is statically invoked.
    static EnumUtil()
    {
        // Throw Exception on static initialization if the given type isn't an enum.
        Require.That(typeof (T).IsEnum, () => typeof(T).FullName + " is not an enum type.");
    }

    /// <summary>
    /// In the .NET Framework, objects can be cast to enum values which are not
    /// defined for their type. This method provides a simple fail-fast check
    /// that the enum value is defined, and creates a cast at the same time.
    /// Cast the given value as the given enum type.
    /// Throw an exception if the value is not defined for the given enum type.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="enumValue"></param>
    /// <exception cref="InvalidCastException">
    /// If the given value is not a defined value of the enum type.
    /// </exception>
    /// <returns></returns>
    public static T DefinedCast(object enumValue)

    {
        if (!System.Enum.IsDefined(typeof(T), enumValue))
            throw new InvalidCastException(enumValue + " is not a defined value for enum type " +
                                           typeof (T).FullName);
        return (T) enumValue;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="enumValue"></param>
    /// <returns></returns>
    public static T Parse(string enumValue)
    {
        var parsedValue = (T)System.Enum.Parse(typeof (T), enumValue);
        //Require that the parsed value is defined
        Require.That(parsedValue.IsDefined(), 
            () => new ArgumentException(string.Format("{0} is not a defined value for enum type {1}", 
                enumValue, typeof(T).FullName)));
        return parsedValue;
    }

    public static bool IsDefined(T enumValue)
    {
        return System.Enum.IsDefined(typeof (T), enumValue);
    }

}

public static class EnumExtensions
{
    public static bool IsDefined<T>(this T enumValue)
        where T : struct, IConvertible
    {
        return EnumUtil<T>.IsDefined(enumValue);
    }
}

Таким образом, я могу сказать:

if(!sEnum.IsDefined()) throw new Exception(...);

... или же:

EnumUtil<Stooge>.Parse(s); // throws an exception if s is not a defined value.

Обновить

Как отметил в комментариях Брэндон Крамер, в С# 7.3 были введены некоторые новые универсальные типы, которые позволяют where T: struct, IConvertible приведенную выше информацию where T: struct, IConvertible на where T: Enum, чтобы улучшить проверку Enum-ness типа во время компиляции. таким образом вы можете избавиться от оператора guard в статическом конструкторе EnumUtil.

Ответ 2

Перечисление просто технически является int (или тем, что вы определили для базового типа перечисления). вы можете проверить соответствующее значение в перечислении, но с вызовом Enum.IsDefined. Подробнее здесь: Вставить int для перечисления на С#

Ответ 3

Enum - действительно тонкая оболочка поверх int. В основном это int + статический набор возможных значений (вид констант). Все проверки находятся во время компиляции, проверки типов и т.д. Но когда вы на самом деле отбрасываете int в enum, время выполнения не волнует. Поэтому подтвердите свой ввод!

Ответ 4

Я изменил реализацию с fooobar.com/questions/45140/... чтобы устранить две проблемы

  1. DefinedCast(object enumValue) указывает, что код может использоваться с string типом и типом int (а также без необходимости позже).
  2. Enum.IsDefined/Enum.Parse оба выделяют массив через Enum.GetValues(typeof(TEnum)) что фактически вызвало замедление игл для моего Enum.GetValues(typeof(TEnum)) использования - этого можно избежать за счет кэширования карты.

Отсюда я и завелся

public static class EnumExtensions
{
    public static TEnum DefinedCast<TEnum>(string value)
        where TEnum : struct, IComparable, IFormattable, IConvertible
    {
        if (!MapByString<TEnum>.Instance.TryGetValue(value, out TEnum @enum))
        {
            throw new InvalidCastException(FormattableString.Invariant($"'{value}' is not a defined value"));
        }

        return @enum;
    }

    public static TEnum DefinedCast<TEnum>(int value)
        where TEnum : struct, IComparable, IFormattable, IConvertible
    {
        if (!MapByInteger<TEnum>.Instance.TryGetValue(value, out TEnum @enum))
        {
            throw new InvalidCastException(FormattableString.Invariant($"'{value}' is not a defined value"));
        }

        return @enum;
    }

    private static class MapByInteger<TEnum>
        where TEnum : struct, IComparable, IFormattable, IConvertible
    {
        public static readonly Dictionary<int, TEnum> Instance = ((TEnum[])Enum.GetValues(typeof(TEnum))).ToDictionary(e => (int)Convert.ChangeType(e, typeof(int), CultureInfo.InvariantCulture));
    }

    private static class MapByString<TEnum>
        where TEnum : struct, IComparable, IFormattable, IConvertible
    {
        public static readonly Dictionary<string, TEnum> Instance = ((TEnum[])Enum.GetValues(typeof(TEnum))).ToDictionary(e => e.ToString(CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase);
    }
}

Ответ 5

Используйте int.Parse(), если вы не создадите исключение в случае, если переданное значение не является разборным. Используйте int.TryParse(), если вы не будете разбирать значение, которое может быть недействительным без исключения исключения.