Confusion with parsing an Enum

2019-02-17 19:10发布

I am converting a numeric value that is of string type into a corresponding Enum. While I was testing my code, I discovered interesting behavior that has me confused.

Using the code example below, can somebody shed light on why an exception isn't thrown if/when the "s" variable has a value that doesn't match one of the Enum values? Also, how is that the sEnum var can be set to a value that doesn't exist within the definition of the Stooge enum?

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

标签: c# parsing enums
5条回答
爷、活的狠高调
2楼-- · 2019-02-17 19:36

I changed the implementation from https://stackoverflow.com/a/4892571/275388 to remedy two issues

  1. The DefinedCast(object enumValue) signature indicates that the code can be used with string and int types (and also needlessly boxes the later).
  2. Enum.IsDefined/Enum.Parse both allocate an array through Enum.GetValues(typeof(TEnum)) which actually caused needles slowdown for my use-case - this can be avoided at the expense of caching a map.

Hence I wound up with

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);
    }
}
查看更多
三岁会撩人
3楼-- · 2019-02-17 19:40

Use int.Parse() if you wan't an exception to be thrown in the case the value passed is not parsable. Use int.TryParse() if you wan't to parse a value that might be invalid without an exception to be thrown.

查看更多
Fickle 薄情
4楼-- · 2019-02-17 19:43

This was a decision on the part of the people who created .NET. An enum is backed by another value type (int, short, byte, etc), and so it can actually have any value that is valid for those value types.

I personally am not a fan of the way this works, so I made a series of utility methods:

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

This way, I can say:

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

... or:

EnumUtil<Stooge>.Parse(s); // throws an exception if s is not a defined value.
查看更多
Luminary・发光体
5楼-- · 2019-02-17 19:48

Enum is really thin wrapper over int. Basically it is int + static collection of possible values (sort of constants). All the checks are at compile time, type checking etc. But when you actually cast int to enum runtime doesn't care. So validate your input!

查看更多
乱世女痞
6楼-- · 2019-02-17 19:59

An enum is just technically an int (or whatever you have defined the enum's underlying type to be). you can check for a corresponding value in the enum, though with a call to Enum.IsDefined. More info here: Cast int to enum in C#

查看更多
登录 后发表回答