9

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);
        }
    }
}
dtb
  • 213,145
  • 36
  • 401
  • 431
Jed
  • 10,649
  • 19
  • 81
  • 125

5 Answers5

12

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.

Update

As Brandon Kramer pointed out in the comments, C# 7.3 introduced some new generic types that allow the where T : struct, IConvertible above to be replaced with where T : Enum, to get better compile-time checking of the Enum-ness of the type being passed in. That way you can get rid of the guard statement in EnumUtil's static constructor.

StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • 1
    `Require.That` also comes from my own library, by the way. You can replace it with `if(!...) throw new Exception(...);` – StriplingWarrior Feb 03 '11 at 22:47
  • I like the Require.That would you share how you did it? – Kirsten Apr 28 '14 at 00:01
  • `parsedValue.IsDefined()` doesn't work for me - `IsDefined()` won't resolve(?) - I'm using `if(!...` if that's relevant. – Simon Heffer Aug 31 '17 at 07:38
  • @SimonHeffer: When debugging extension methods, I find it useful to start by invoking them directly: `EnumUtil.IsDefined(parsedValue)`. Once you do that, the IDE tends to give you more useful error messages. It's possible you're missing a `using` statement or a reference, or that `parsedValue`'s type is not actually an enum. – StriplingWarrior Aug 31 '17 at 16:55
  • @StriplingWarrior - I think it may be that I'm on C#5. Maybe using IsDefined as a method on an enum object is not supported yet? (As opposed to the syntax of passing object as parameter. This works: `public static T Parse(string enumValue) { var parsedValue = (T)System.Enum.Parse(typeof(T), enumValue); if (!IsDefined(parsedValue)) throw new ArgumentException(string.Format("{0} is not a defined value for enum type {1}", enumValue, typeof(T).FullName)); return parsedValue; }` – Simon Heffer Sep 01 '17 at 07:30
  • @SimonHeffer: I just realized that I omitted the extension method which makes that syntax work. Check out the `EnumExtensions` class in my edit. – StriplingWarrior Sep 01 '17 at 20:18
  • @StriplingWarrior Is there some reason you can't just use `where T : struct, System.Enum` as the constraint to get very good static type checking for enums? – Brandon Kramer Jun 17 '19 at 13:49
  • @BrandonKramer: At the time this was written that wasn't allowed by the language. It was [introduced in C# 7.3](https://devblogs.microsoft.com/premier-developer/dissecting-new-generics-constraints-in-c-7-3/). I'll add an update about that, though. – StriplingWarrior Jun 17 '19 at 15:24
  • @StriplingWarrior Oh wow, I didn't realize that was such a recent addition! – Brandon Kramer Jun 17 '19 at 16:47
3

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#

Community
  • 1
  • 1
Gabriel Magana
  • 4,338
  • 24
  • 23
1

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!

Andrey
  • 59,039
  • 12
  • 119
  • 163
0

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);
    }
}
Mads Ravn
  • 619
  • 6
  • 18
-1

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.

CodeZombie
  • 5,367
  • 3
  • 30
  • 37
  • 1
    That's not quite what he's going for. He wants to have an exception thrown if the value is not a defined value for his enum type, regardless of whether it is a valid int value. – StriplingWarrior Feb 03 '11 at 22:45