Programming Practices – What Should Be the Last Entry in a Switch/Case Statement?

default valueslanguage-agnosticpatterns-and-practicesprogramming practicesswitch statement

When writing a switch statement that only ever has to deal with a known set of values (imagine an implicit enumeration), I find myself wondering what should be the last entry in the construct. I'm always considering the same 3 approaches, but I'd really like to just stick with the one that makes the most sense and never think about this again. The alternatives are described below. For the sake of discussion, let's assume there are exactly 4 options for the "switching variable".

  • Approach #1: 4 cases, no default case.
  • Approach #2: 3 cases, default acts as the last case (+ comment explaining this).
  • Approach #3: 4 cases, default case with a "ShouldNeverHappenException".

My thoughts on this are as follows:

  • One the one hand, since default is effectively a case that cannot be reached, it seems pointless to clutter the code with it.
  • On the other hand, it's bad not to handle a default case out of of future-proofing considerations, that is, if someday another option becomes available (due to e.g. API changes outside of my control), the code might react to it incorrectly if the default case is used for one of the expected options.
  • If the last case handles an error scenario, it might not be so bad if it's the default behavior (assuming I have no fine-grained error handling).

Based on the above reasoning I tend to prefer the 3rd approach, but I am no software engineer, and perhaps I'm missing something.

What are the best practices in this case?

Best Answer

imagine an implicit enumeration

I think this is the key point. Implicit means not an actual enum type, but, say, numbers with special meaning.

const int A = 1;
const int B = 2;
const int C = 3;

And a method that is using the switch statement:

public void DoSomething(int type)
{
    switch (type)
    {
        case A:
            // Stuff
            break;
        case B:
            // Stuff
            break;
        case C:
            // Stuff
            break;
        default:
            // Do I even needs this?
            break;
    }
}

The "implicit" enumeration in this example is a 32 bit integer. The entire range of values for the 32 bit integer are your possible cases. So what if I call:

DoSomething(355);

Or:

DoSomething(-2);

You might say "I'm always using the constants A, B and C in my program when calling this method, so it's impossible to pass -1"

But the compiler allows it, therefore a code change in the future in how DoSomething is called can pass an illegal value.

If you pass an illegal value, is there an intelligent default you can fall back on? If so, that's your default. If not, throw an exception.

I would argue for a default even for an explicit enumeration:

enum Foo
{
    A = 1,
    B = 2,
    C = 3
}

public void DoSomething(Foo type)
{
    switch (type)
    {
        case A:
            // Stuff
            break;
        case B:
            // Stuff
            break;
        case C:
            // Stuff
            break;
        default:
            throw new NotImplementedException("Type " + type + " is not yet implemented");
            break;
    }
}

The default clause throwing an exception to the fact a value is not implemented is a good debugging tool for the future where you add an item to the enum, and then you forget to change the application behavior based on that.

Will the exception happen in production? Most likely not. It will probably happen during development or testing and be fixed.

But that's the benefit of having a default clause and either doing something by default or blowing up.

There are some cases where doing nothing is OK. I honestly would still put in a default clause with a comment:

public void DoSomething(int type)
{
    switch (type)
    {
        case A:
            ...
            break;
        case B:
            ...
            break;
        case C:
            ...
            break;
        default:
            // Do nothing intentionally
            break;
    }
}

To me this falls under the same guidelines as exception handling. If you catch the exception, do something with it. If you don't want to do anything with it, catch the exception and explain why nothing is done:

try
{
    ...
}
catch(Exception)
{
    // Do nothing, because we are trying to do something and we
    // really don't care if it happens.
}