Mark
Mark

Reputation: 2432

Switch statement without default when dealing with enumerations

This has been a pet peeve of mine since I started using .NET but I was curious in case I was missing something. My code snippet won't compile (please forgive the forced nature of the sample) because (according to the compiler) a return statement is missing:

public enum Decision { Yes, No}

    public class Test
    {
        public string GetDecision(Decision decision)
        {
            switch (decision)
            {
                case Decision.Yes:
                    return "Yes, that's my decision";
                case Decision.No:
                    return "No, that's my decision";

            }
        }
    }

Now I know I can simply place a default statement to get rid of the complier warning, but to my mind not only is that redundant code, its dangerous code. If the enumeration was in another file and another developer comes along and adds Maybe to my enumeration it would be handled by my default clause which knows nothing about Maybes and there's a really good chance we're introducing a logic error.

Whereas, if the compiler let me use my code above, it could then identify that we have a problem as my case statement would no longer cover all the values from my enumeration. Sure sounds a lot safer to me.

This is just so fundamentally wrong to me that I want to know if its just something I'm missing, or do we just have to be very careful when we use enumerations in switch statements?

EDIT: I know I can raise exceptions in the default or add a return outside of the switch, but this are still fundamentally hacks to get round a compiler error that shouldn't be an error.

With regards an enum really just being an int, that's one of .NET's dirty little secrets which is quite embarassing really. Let me declare an enumeration with a finite number of possibilities please and give me a compilation for:

Decision fred = (Decision)123;

and then throw an exception if somebody tries something like:

int foo = 123;
Decision fred = (Decision)foo;

EDIT 2:

A few people have made comments about what happens when the enum is in a different assembly and how this would result in problems. My point is that this is the behaviour I think should happen. If I change a method signature this will lead to issues, my premis is that changing an enumeration should be this same. I get the impression that a lot of people don't think I understand about enums in .NET. I do I just think that the behaviour is wrong, and I'd hoped that someone might have known about some very obscure feature that would have altered my opinion about .NET enums.

Upvotes: 43

Views: 25032

Answers (10)

Eric Lippert
Eric Lippert

Reputation: 660038

Heck, the situation is far worse than just dealing with enums. We don't even do this for bools!

public class Test {        
  public string GetDecision(bool decision) {
    switch (decision) {
       case true: return "Yes, that's my decision";                
       case false: return "No, that's my decision"; 
    }
  }
}

Produces the same error.

Even if you solved all the problems with enums being able to take on any value, you'd still have this issue. The flow analysis rules of the language simply do not consider switches without defaults to be "exhaustive" of all possible code paths, even when you and I know they are.

I would like very much to fix that, but frankly, we have many higher priorities than fixing this silly little issue, so we've never gotten around to it.


UPDATE: Low-bang-for-buck features that do not justify the effort on their own merits sometimes get done when another language feature needs it, and that has happened here. C# needed exhaustivity checks for the improved pattern matching and the switch expression. The rules are:

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/patterns#114-pattern-exhaustiveness

Upvotes: 49

B H
B H

Reputation: 1878

Instead of complaining about how the switch statement works, avoid it entirely by using an extension method on the enum as explained here and here.

The nice thing about this approach is, you don't get into a situation and forgetting to update your GetDecision switch statement when adding a new enum value because it's all together in the same place - in the enum declaration.

The efficiency of this approach is not known to me and, really, not even a consideration at this point. That's right, I just don't care because it's so much easier for me that I figure, "Phhhtt screw the computer that's what it's there for - to work hard." (I may regret that attitude when Skynet takes over.)

If I ever need to get from one of those attribute values back to the enum value, I can simply build a reverse dictionary of and fill it with a single line of code.

(I usually do a 'not set' as the first enum element because, as the OP notes, a C# enum is really an int so an uninitialized variable is going to be zero or the first enum value)

public enum Decision
{
  [DisplayText("Not Set")]
  NotSet,
  [DisplayText("Yes, that's my decision")]
  Yes,
  [DisplayText("No, that's my decision")]
  No
}

public static class XtensionMethods
{
  public static string ToDisplayText(this Enum Value)
  {
    try
    {
      Type type = Value.GetType();
      MemberInfo[] memInfo =
        type.GetMember(Value.ToString());

      if (memInfo != null && memInfo.Length > 0)
      {
        object[] attrs = memInfo[0]
          .GetCustomAttributes(typeof(DisplayText), false);
        if (attrs != null && attrs.Length > 0)
          return ((DisplayText)attrs[0]).DisplayedText;
      }
    }
    catch (Exception ex)
    {
      throw new Exception(
        "Error in XtensionMethods.ToDisplayText(Enum):\r\n" + ex.Message);
    }
    return Value.ToString();
  }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class DisplayText : System.Attribute
  {
    public string DisplayedText;

    public DisplayText(string displayText)
    {
      DisplayedText = displayText;
    }
  }
}

Use inline like:

myEnum.ToDisplayText();

Or wrapped in a function, if you feel like it:

public string GetDecision(Decision decision)
{
  return decision.ToDisplayText();
}

Upvotes: 0

AnorZaken
AnorZaken

Reputation: 2114

For the sake of sharing a quirky idea if nothing else, here goes:

You can always implement your own strong enums

...and since the introduction of the nameof operator you can also use them in switch-cases. (Not that you couldn't technically do so previously, but it was difficult to make such code readable and refactor friendly.)

public struct MyEnum : IEquatable<MyEnum>
{
    private readonly string name;
    private MyEnum(string name) { name = name; }

    public string Name
    {
        // ensure observable pureness and true valuetype behavior of our enum
        get { return name ?? nameof(Bork); } // <- by choosing a default here.
    }

    // our enum values:
    public static readonly MyEnum Bork;
    public static readonly MyEnum Foo;
    public static readonly MyEnum Bar;
    public static readonly MyEnum Bas;

    // automatic initialization:
    static MyEnum()
    {
        FieldInfo[] values = typeof(MyEnum).GetFields(BindingFlags.Static | BindingFlags.Public);
        foreach (var value in values)
            value.SetValue(null, new MyEnum(value.Name));
    }

    /* don't forget these: */
    public override bool Equals(object obj)
    {
        return obj is MyEnum && Equals((MyEnum)obj);
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
    public override string ToString()
    {
        return Name.ToString();
    }
    public bool Equals(MyEnum other)
    {
        return Name.Equals(other.Name);
    }
    public static bool operator ==(MyEnum left, MyEnum right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyEnum left, MyEnum right)
    {
        return !left.Equals(right);
    }
}

and use it thusly:

public int Example(MyEnum value)
{
    switch(value.Name)
    {
        default: //case nameof(MyEnum.Bork):
            return 0;
        case nameof(MyEnum.Foo):
            return 1;
        case nameof(MyEnum.Bar):
            return 2;
        case nameof(MyEnum.Bas):
            return 3;
    }
}

and you would of course call that method like so:
int test = Example(MyEnum.Bar); // returns 2

That we can now easily get the Name is basically just a bonus, and yeah some readers might point out that this is basically a Java enum without the null-case (since it's not a class). And just like in Java you can add whatever extra data and or properties you desire to it, e.g. an ordinal value.

Readability: Check!
Intellisense: Check!
Refactorability: Check!
Is a ValueType: Check!
True enumeration: Check!
...
Is it performant? Compared to native enums; no.
Should you use this? Hmmm....

How important is it for you to have true enumerations so you can getting rid of enum runtime checks and their accompanying exceptions?
I don't know. Can't really answer that for you dear reader; to each their own.

...Actually, as I wrote this I realized it would probably be cleaner to let the struct "wrap" a normal enum. (The static struct fields and the corresponding normal enum mirroring each other with the help of similar reflection as above.) Just never use the normal enum as a parameter and you're good.

UPDATE :

Yepp, spent the night testing out my ideas, and I was right: I now have near perfect java-style enums in c#. Usage is clean and performance is improved. Best of all: all the nasty shit is encapsulated in the base-class, your own concrete implementation can be as clean as this:

// example java-style enum:
public sealed class Example : Enumeration<Example, Example.Const>, IEnumerationMarker
{
    private Example () {}

    /// <summary> Declare your enum constants here - and document them. </summary>
    public static readonly Example Foo = new Example ();
    public static readonly Example Bar = new Example ();
    public static readonly Example Bas = new Example ();

    // mirror your declaration here:
    public enum Const
    {
        Foo,
        Bar,
        Bas,
    }
}

This is what you can do:

  • You can add any private fields you want.
  • You can add any public non-static fields you want.
  • You can add any properties and methods you want.
  • You can design your constructors however you wish, because:
  • You can forget base constructor hassle. Base constructor is parameter-less!

This is what you must do:

  1. Your enum must be a sealed class.
  2. All your constructors must be private.
  3. Your enum must inherit directly from Enumeration<T, U> and inherit the empty IEnumerationMarker interface.
  4. The first generic type parameter to Enumeration<T, U> must be your enum class.
  5. For each public static field there must exist an identically named value in the System.Enum (that you specified as the second generic type parameter to Enumeration<T, U>).
  6. All your public static fields must be readonly and of your enum type.
  7. All your public static fields must be assigned a unique non-null value during type initialization.

At the moment every invariant above is asserted at type initialization. Might try to tweak it later to see if some of it can be detected at compile-time.

Requirements Rationale:

  1. Your enum must be sealed because if it isn't then other invariants become a lot more complicated for no obvious benefit.
  2. Allowing public constructors makes no sense. It's an enumeration type, which is basically a singleton type but with a fixed set of instances instead of just one.
  3. Same reason as the first one. The reflection and some of the other invariant and constraint checking just becomes messy if it isn't.
  4. We need this generic type parameter so the type-system can be used to uniquely store our enum data with performant compile/Jit time bindings. No hash-tables or other slow mechanisms are used! In theory it can be removed, but I don't think it's worth the added cost in complexity and performance to do so.
  5. This one should be quite obvious. We need these constants for making elegant switch-statements. Of course I could make a second enum-type that doesn't have them; you would still be able to switch using the nameof method shown earlier. It just would not be as performant. I'm still contemplating if a should relax this requirement or not. I'll experiment on it...
  6. Your enum constants must be public and static for obvious reasons, and readonly fields because a) having readonly enumeration instances means all equality checks simplifies to reference equality; b) properties are more flexible and verbose, and frankly both of those are undesirable properties for an enumeration implementation. Lastly all public static fields must be of your enum-type simply because; a) it keeps your enumeration type cleaner with less clutter; b) makes the reflection simpler; and c) you are free to do whatever with properties anyway so it's a very soft restriction.
  7. This is because I'm trying hard to keep "nasty reflection magic" to a minimum. I don't want my enum implementation to require full-trust execution. That would severely limit its usefulness. More precisely, calling private constructors or writing to readonly fields can throw security exceptions in a low-trust environment. Thus your enum must instantiate your enum constants at initialization time itself - then we can populate the (internal) base-class data of those instances "cleanly".

So anyway, how can you use these java-style enums?

Well I implemented this stuff for now:

int ordinal = Example.Bar.Ordinal; // will be in range: [0, Count-1]
string name = Example.Bas.Name; // "Bas"
int count = Enumeration.Count<Example>(); // 3
var value = Example.Foo.Value; // <-- Example.Const.Foo

Example[] values;
Enumeration.Values(out values);

foreach (var value in Enumeration.Values<Example>())
    Console.WriteLine(value); // "Foo", "Bar", "Bas"

public int Switching(Example value)
{
    if (value == null)
        return -1;

    // since we are switching on a System.Enum tabbing to autocomplete all cases works!
    switch (value.Value)
    {
        case Example.Const.Foo:
            return 12345;
        case Example.Const.Bar:
            return value.GetHasCode();
        case Example.Const.Bas:
            return value.Ordinal * 42;
        default:
            return 0;
    }
}

The abstract Enumeration class will also implement the IEquatable<Example> interface for us, including == and != operators that will work on Example instances.

Aside from the reflection needed during type initialization everything is clean and performant. Will probably move on to implement the specialized collections java has for enums.

So where is this code then?

I want to see if I can clean it up a bit more before I release it, but it will probably be up on a dev branch on GitHub by the end of the week - unless I find other crazy projects to work on! ^_^

Now up on GitHub
See Enumeration.cs and Enumeration_T2.cs.
They are currently part of the dev branch of a very much wip library I'm working on.
(Nothing is "releasable" yet and subject to breaking changes at any moment.)
...For now the rest of the library is mostly a shit ton of boilerplate to extend all array methods to multi-rank arrays, make multi-rank arrays usable with Linq, and performant ReadOnlyArray wrappers (immutable structs) for exposing (private) arrays in a safe way without the cringy need to create copies all the time.

Everything* except the very latest dev commits is fully documented and IntelliSense friendly.
(*The java enum types are still wip and will be properly documented once I've finalized their design.)

Upvotes: 2

deegee
deegee

Reputation: 1623

I realize that this is a thread resurrection...

I personally feel that the way that switch works is correct, and it operates how I would logically want it to.

I'm surprised to hear such complaining about the default label.

If you only have a strict set of enums or values that you are testing, you don't need all of the exception handling lines, or a return outside of the switch, etc.

Just put the default label over one of the other labels, perhaps the label that would be the most common response. In your example case it probably doesn't matter which one. Short, sweet, and it fulfills your needs to rid of the compiler warning:

switch (decision)
{
    default:
    case Decision.Yes:
        return "Yes, that's my decision";
    case Decision.No:
        return "No, that's my decision";
}

If you don't want the default to be Yes, put the default label above the No label.

Upvotes: 2

Timothy Carter
Timothy Carter

Reputation: 15785

In addition to the case of, you can cast any int to your enum and have an enum you aren't handling. There is also the case where, if the enum is in an external .dll, and that .dll is updated, it does not break your code if an additional option is added to the enum (like Yes, No, Maybe). So, to handle those future changes you need the default case as well. There is no way to guarantee at compile time that you know every value that enum will have for it's life.

Upvotes: 0

Dimi Takis
Dimi Takis

Reputation: 4949

public enum Decision { Yes, No}

public class Test
{
    public string GetDecision(Decision decision)
    {
        switch (decision)
        {
            case Decision.Yes:
                return "Yes, that's my decision";
            case Decision.No:
                return "No, that's my decision";
            default: throw new Exception(); // raise exception here.

        }
    }
}

Upvotes: 9

Thomas Levesque
Thomas Levesque

Reputation: 292425

That's because the value of decision could actually be a value that is not part of the enumeration, for instance :

string s = GetDecision((Decision)42);

This kind of thing is not prevented by the compiler or the CLR. The value could also be a combination of enum values :

string s = GetDecision(Decision.Yes | Decision.No);

(even if the enum doesn't have the Flags attribute)

Because of that, you should always put a default case in you switch, since you can't check all possible values explicitly

Upvotes: 20

Bryan Watts
Bryan Watts

Reputation: 45445

Throw an exception in the default clause:

default:
    throw new ArgumentOutOfRangeException("decision");

This ensures that all possible paths are covered, while avoiding the logic errors resulting from adding a new value.

Upvotes: 27

Maestro1024
Maestro1024

Reputation: 3293

I always think of that default as the fall through/exception.

So here it would not be maybe but instead would be "Invalid Decision, contact support".

I don't see how it would fall through to that but that would be the catchall/exception case.

Upvotes: 1

Joel Goodwin
Joel Goodwin

Reputation: 5086

The default is there to protect you. Throw an exception from the default and if anyone adds an extra enum, you're covered with something to flag it.

Upvotes: 3

Related Questions