10

This has always bugged me. Perhaps someone with some hardcore knowledge of .NET internals can explain it to me.

Suppose I define an enum as follows:

public enum Foo
{
   Eenie = 1,
   Meenie = 2,
   Miney = 3,
   Moe = 4
}

Now, also suppose that somewhere in my code, I have the following code:

int bar = (Foo)5;

This will compile just fine, and no exception will be raised whatsoever, even though the value 5 is clearly not a valid value defined in Foo.

Or, consider the following:

public void ProcessFoo(Foo theFoo)
{
    // Do processing
}

public static void Main()
{
    ProcessFoo((Foo)5);
}

Again, no exception.

In my mind, this should result in a type mismatch exception, because 5 is not a Foo. But the designers chose not to do that.

Now, I've written an extension method that can verify that this is the case, and it's no big deal to invoke it to ensure that that's the case, but I have to use reflection to do it (with all its performance penalties and whatnot).

So again, what compelling reason is there that could possibly have driven the decision to not have a checked enum?

For reference, from the MSDN documentation for the Enum class:

When you define a method or property that takes an enumerated constant as a value, consider validating the value. The reason is that you can cast a numeric value to the enumeration type even if that numeric value is not defined in the enumeration.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
Mike Hofer
  • 16,477
  • 11
  • 74
  • 110
  • 3
    I don't want to start a holy war here, but that's the reason why I like Java enums more: In Java enum types are special classes and enum values are objects instead of ints with some special meaning. This means that you can't even pass an invalid value into a method that expects an enum (except null) – Joachim Sauer Jan 11 '09 at 13:45

10 Answers10

17

The issue was performance. It is quite simple to have a checked enum for normal enums such as Color

enum Color {
  Red,
  Blue
}

The problem though is for enum's which are used as bit flags.

enum Property {
  IsFirst = 0x1,
  IsDefault = 0x2,
  IsLastAccessed = 0x4
}

Having to do a bitwise check for every single integer which is converted to an Enum value was deemed to be too expensive. Hence the relaxed conversion to enum values.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
  • 3
    So why not just create a modifier or different type of enum? Like strict enum, or CheckedEnum, or something similar? For 99% of users, performance is not an issue ... but allowing enums with bogus values is an issue. – Beep beep Nov 30 '10 at 02:24
  • @Beepbeep: As with most questions that boil down to "X is possible, why not do X?", the answer is "Because no one was bothered enough by it to actually do so". – Flater Nov 23 '21 at 16:14
  • Validating a [Flags] enum would actually be faster, since you can just "or" all values to get the valid mask, and then check values against that mask. For non-flags enums you will have to check against all the defined values. – JacquesB Jan 03 '22 at 19:14
4

Range-checking has a potentially unnecessary cost. It's therefore reasonable to not perform it implicitly. As already mentioned, [Flags] require that no such checking takes place. If the runtime would check for the presence of [Flags], this would still incur a runtime penalty every time you perform a conversion.

The only way around this would be for the compiler to be aware of the [Flags] attribute. I guess this wasn't done to reduce the amount of runtime knowledge hardcoded into the compiler.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • This answer is correct, but as Konrad says, JaredPar's answer (http://stackoverflow.com/questions/432937/-net-why-arent-enums-range-value-checked/433084#433084) is the canonical answer. – Scott Dorman Apr 10 '09 at 20:27
2

If they are range checked, how would you have [Flags] enums and combine them using bitwise or?

An example would be ControlStyles enum

Mehrdad Afshari
  • 414,610
  • 91
  • 852
  • 789
1

Two reasons jump to mind. First, generating an out of range value requires a cast. If you intentionally cast, why would you expect to be slapped at runtime? It would have been far easier to disallow the cast.

Another compelling one is this one:

enum VeryHardToRangeCheck {
  one = 1,
  three = 3,
  five = 5
}
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
1

I can see two reasons:

  1. [Flags] work smoother without checks. For your example,

    (Foo)5 == Foo.Eenie | Foo.Moe;

  2. Enum is a value type. If you don't initialize it, it will be equal to zero. If you want to have checked enum values, it's not clear when exception should be thrown in this case - zero value may sneak to you code when you, for example, create an instance of a class containing this enum as a field.

    So current behavior is more consistent - you just know that you can have out-of-range values and check them.

Moreover, you should always explicitly do your checks and throw exceptions for values you can't process. Otherwise addition of new values to your enumeration may change behavior of your existing code. Fortunately is you use a single switch statement is a method which returns value, compiler will make you explicitly specify what you want to do if no matches found - in default section of switch of after it you will have to return a value or throw an exception. NotSupportedExcpetion of preferred in most cases.

Konstantin Spirin
  • 20,609
  • 15
  • 72
  • 90
0

This is because (Foo)5 is Foo.Eenie | Foo.Moe

Pablo Retyk
  • 5,690
  • 6
  • 44
  • 59
  • This would only be the case when [Flags] is used. Now you can also say that (Foo)5 is Foo.Meenie | Foo.Miney. – Ruben Jan 11 '09 at 13:49
  • yes it can also be Foo.Meenie | Foo.Miney, but this is not the compiler's problem, maybe you meant it to work it that way, Mike Hofer only asked why isn't this a compilation error. Of course that you can use [Flags] or use other 2 exp values. – Pablo Retyk Jan 11 '09 at 13:55
  • Yes, but, in allowing Enum and Flags to be the same type, C# is perpetuating a design mistake from C. – Anthony Jan 11 '09 at 14:12
0

I'd say the reason is because the enums are only type checked and in-lined by the compilier at compile time. Especially useful for extending enums with the Flags attribute (as it suddenly becomes forward compatible...

Rowland Shaw
  • 37,700
  • 14
  • 97
  • 166
0

Enums are range and value checked. You just explicitly circumvented the check with the (Foo)5 syntax.

C# is a statically typed language which means that all types are verified at compile time. The compiler proves that no type errors exist in the program and therefore it is not necessary to check types are runtime - they are guaranteed to be correct.

Checking types at runtime is impossible for value types, since value types do not carry type information at runtime. For example, there is no way to tell at runtime if an integer is signed or unsigned. Similarly for enums, they are just integers at runtime and do not carry any information about what enum they are. The type safety happens purely at compile time.

For reference types, it is different because they do carry type information around at runtime, which is why the type can be accessed at runtime with GetType(). But this is not the case for value types. (This is part of what makes value types small and fast compared to reference types.)

But trouble arises when you use the (Foo)bar type cast expression. Type casts are basically an escape hatch in the type system for cases where you know better than the compiler. You tell the compiler that you know bar is a valid Foo, and the compiler should trust you.

Type casts are one of the most confusing parts of C# because they work completely different for value types and for reference types. For reference types, they perform a runtime check that the value indeed has the specified type. If not, it throws an exception.

But for value types no such check is possible. Instead, a cast just treats the value as the expected type and let you take responsibility for verifying if this would be a valid operation. Unverified casts can lead to all kinds of unexpected results. Take this code:

var a = -1;
var b = (uint)a;

What is the value of b? If you guessed 4294967295 you guessed correctly.

So bottom line, when casting value types, you have the responsibility to ensure the value is valid for the type.

In almost all real-world cases, you should validate input, e.g. by using Enum.IsDefined. The raw cast should only be used when you are absolutely sure the value is valid. But even then, be aware that enums can be wrong. Eg. consider this code:

    enum Color {
        Red,
        Blue
    }
    enum Hat {
        Cab,
        TopHat
    }
    public static void Main(){
        var a = Color.Blue;
        var b = (int)a;
        if (!Enum.IsDefined<Hat>(b)) throw new Exception();
        var c = (Hat)b;
        Console.Write(c); // writes TopHat
    }

The validation can only check that the valid is valid for the given enum, but information about which enum the value initially belonged to is lost. So this is markedly different from reference types which always retain their type information and never change type at runtime.

JacquesB
  • 41,662
  • 13
  • 71
  • 86
0

Microsoft's C# Programming Guide specifically says not to do what you are asking about:

It is possible to assign any arbitrary integer value to meetingDay. For example, this line of code does not produce an error: meetingDay = (Days) 42. However, you should not do this because the implicit expectation is that an enum variable will only hold one of the values defined by the enum. To assign an arbitrary value to a variable of an enumeration type is to introduce a high risk for errors.

int is really only the storage type. In fact, it's possible to specify other integer storage types, like byte:

enum Days : byte {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};

The storage type determines how much memory an enum uses.

JeffH
  • 10,254
  • 2
  • 27
  • 48
-2

Sorry to necro. Is it possible that you're confusing an Enum with a key-value collection here?

When you assign an integer Meenie=2 to an enum item, all you're doing is saying that Meenie is the 3rd index, and everything after it, if not specified, will take an index of 2+(distance from Meenie). So when you look for Foo[5], you're looking for index 5, not some key which has 5 as a value.

In most cases you wouldn't do this anyway; you'd ask for Foo.Meenie - that's the point of the enum, to set a known range of values and then refer to them by their exposed names. It's just a developer convenience. There are better structures out there for doing what you do in your example.

  • I have no idea whatsoever how enumerations correlate to key-value pairs in this context. An enumeration is an enumeration: a set of strongly typed constants. Key-value pairs are altogether different (think, a hash table or dictionary), typically designed for fast searching. My point, in the original post, is that an enumeration could be range checked in some way, but others have pointed out why that's not the case. – Mike Hofer Oct 14 '10 at 18:26