157

If I have an enum like so:

enum Beer
{
    Bud = 10,
    Stella = 20,
    Unknown
}

Why does it not throw an exception when casting an int that is outside of these values to a type of Beer?

For example the following code doesn't throw an exception, it outputs '50' to the console:

int i = 50;
var b = (Beer) i;

Console.WriteLine(b.ToString());

I find this strange...can anyone clarify?

jcvandan
  • 14,124
  • 18
  • 66
  • 103

4 Answers4

94

Taken from Confusion with parsing an Enum

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.

Edit

Beyond the explanation given above, you have to realize that the .NET version of Enum follows a more C-inspired pattern than a Java-inspired one. This makes it possible to have "Bit Flag" enums which can use binary patterns to determine whether a particular "flag" is active in an enum value. If you had to define every possible combination of flags (i.e. MondayAndTuesday, MondayAndWednesdayAndThursday), these would be extremely tedious. So having the capacity to use undefined enum values can be really handy. It just requires a little extra work when you want a fail-fast behavior on enum types that don't leverage these sorts of tricks.

Community
  • 1
  • 1
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • 1
    Nice...I'm not a fan of the way it works either, seems bizarre to me – jcvandan Jun 20 '11 at 15:44
  • 3
    @dormisher: "'Tis madness, yet there is method to't." See my edit. – StriplingWarrior Jun 20 '11 at 15:48
  • 2
    @StriplingWarrior, for interest. The Java equivalent is EnumSet.of(Monday, Wednesday, Thursday). Inside the EnumSet is just a single long. So gives a nice API without much efficiency loss. – Iain May 14 '13 at 06:38
  • 2
    "Require.That()" => http://stackoverflow.com/questions/4892548/confusion-with-parsing-an-enum/4892571#comment5443975_4892571 – John Sep 26 '14 at 16:47
  • @StriplingWarrior in your parse method you are trying to call IsDefined like an extension method. However IsDefined method cannot be made an extension method because of the way you try to make EnumUtil class with constraints (to make static check for enum, which i like very much). however in effect it is telling me that one cannot make an extension method on a generic class. Any ideas? – CJC Oct 01 '16 at 19:59
  • @CJC: You're right. I must have had an extension method defined on a non-generic class. I've added some code to my code sample which would make this work. – StriplingWarrior Oct 03 '16 at 15:47
  • @StriplingWarrior Thanks, this was actually what i had in mind too. In fact i even called it the same EnumExtensions hahaha. Ok thanks. have a nice day – CJC Oct 04 '16 at 18:02
  • `DefinedCast` has issues when passing in a valid string. `IsDefined` handles strings, but the string cannot be cast to T. – Jonas Nyrup Jun 08 '17 at 11:34
  • @JonasNyrup: Yeah, I'd never expected people to pass a string into this method, since it's supposed to be a *cast* and not a *parse*. The exception it throws is the same exception that a straight cast would throw, so one could argue that the behavior is correct. However, if you want a better error message, or even want to handle strings, you could easily modify the method. If you choose to handle strings, I'd rename it to DefinedConvert or something like that. – StriplingWarrior Jun 08 '17 at 15:27
63

Enums are often used as flags:

[Flags]
enum Permission
{
    None = 0x00,
    Read = 0x01,
    Write = 0x02,
}
...

Permission p = Permission.Read | Permission.Write;

The value of p is the integer 3, which is not a value of the enum, but clearly is a valid value.

I personally would rather have seen a different solution; I would rather have had the ability to make "bit array" integer types and "a set of distinct value" types as two different language features rather than conflating them both into "enum". But that's what the original language and framework designers came up with; as a result, we have to allow non-declared values of the enum to be legal values.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 24
    But 3 "clearly is a valid value" sort of strengthens the OP's point. That [Flags] attribute could tell the compiler to look for a defined enum or a valid value. Just sayin'. – LarsTech Jun 20 '11 at 17:22
  • Using Enum Flags instead of multiple boolean properties or some record is anyway a very outdated approach. I can understand they made a bad bet at that moment, but I don't see a reason to fix this with a new Enum type in newer versions of C#. Adding a new value to an existing Enum right now is not static typed and you don't know what code paths will break in a large codebase. If you think the C# is a language for the long term, this should be fixed IMHO. – Dirk Boer Apr 18 '23 at 17:24
  • @DirkBoer: I agree with your sentiment, but I wrote this answer 12 years ago and I have not worked at Microsoft for 11 years. If you want a change made to C#, the process is open and on github; take it up with the language designers, not me! – Eric Lippert Apr 18 '23 at 21:45
  • Hi @EricLippert, tnx for your response! It was not specifically directed to you, more as a general discussion point. It always feels bit "unreachable" but actually a good idea to actually bring this up to the language designers. – Dirk Boer Apr 19 '23 at 17:08
19

The short answer: The language designers decided to design the language this way.

The long answer: Section 6.2.2: Explicit enumeration conversions of the C# Language Specification says:

An explicit enumeration conversion between two types is processed by treating any participating enum-type as the underlying type of that enum-type, and then performing an implicit or explicit numeric conversion between the resulting types. For example, given an enum-type E with and underlying type of int, a conversion from E to byte is processed as an explicit numeric conversion (§6.2.1) from int to byte, and a conversion from byte to E is processed as an implicit numeric conversion (§6.1.2) from byte to int.

Basically, the enum is treated as the underlying type when it comes to do a conversion operation. By default, an enum's underlying type is Int32, which means the conversion is treated exactly like a conversion to Int32. This means any valid int value is allowable.

I suspect this was done mainly for performance reasons. By making enum a simple integral type and allowing any integral type conversion, the CLR doesn't need to do all of the extra checks. This means that using an enum doesn't really have any performance loss when compared to using an integer, which in turn helps encourage its use.

Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 3
    Do you find this strange? I thought one of the main point of an enum is to safely group a range of integers that we can attribute individual meanings too. To me allowing an enum type to hold any integer value defeats it's purpose. – jcvandan Jun 20 '11 at 15:42
  • @dormisher: I just edited and added a bit at the end. There is a cost involved in providing the "safety" you're after. That being said, with *normal* usage, this really isn't a problem at all. – Reed Copsey Jun 20 '11 at 15:44
10

From the documentation:

A variable of type Days can be assigned any value in the range of the underlying type; the values are not limited to the named constants.

Martin
  • 39,569
  • 20
  • 99
  • 130