Four switch oddities

Four switch oddities

Rate This
  • Comments 39

The C# switch statement is a bit weird. Today, four quick takes on things you probably didn't know about the switch statement.

Case 1:

You probably know that it is illegal to "fall through" from one switch section to another:

switch(attitude)
{
  case Attitude.HighAndMighty:
    Console.WriteLine("High");
    // we want to fall through, but this is an error
  case Attitude.JustMighty:
    Console.WriteLine("Mighty");
    break;
}

But perhaps you did not know that it is legal to force a fall-through with a goto:

switch(attitude)
{
  case Attitude.HighAndMighty:
    Console.WriteLine("High");
    goto case Attitude.JustMighty;
  case Attitude.JustMighty:
    Console.WriteLine("Mighty");
    break;
}

Pretty neat! Cases are semantically just labels to which the switch does a conditional branch; we let you do an explicit branch if you want to.

Case 2:

A common and confusing error that C programmers using C# (like me!) make all the time:

switch(attitude)
{
  case Attitude.HighAndMighty:
    Console.WriteLine("High and Mighty");
    break;    
  case Attitude.JustMighty:
    Console.WriteLine("Just Mighty");
    break;
  default:
    // Do nothing
}

That's an error because in the default case, you "fall through". Admittedly, there is nothing to "fall through" to, but the compiler is picky about this one. It requires that every switch section, including the last one, have an unreachable end point. The purpose of this rule, and of the no-fall-through rule in general, is that we want you to be able to arbitrarily re-order your switch sections without accidentally introducing a breaking change. Fix it by making the "do nothing" explicit with a break statement.

This is particularly confusing because some people interpret the error message as saying that the problem is falling into the default case, when actually the problem is that you're falling out of the default case.

Case 3:

As I discussed earlier, a declaration space is a region of code in which two declared things may not have the same name. The foreach loop implicitly defines its own declaration space, so this is legal:

foreach(var blah in blahs) { ... }
foreach(var blah in otherblahs) { ... }

Switch blocks also define their own declaration spaces, but switch sections do not:

switch(x)
{
  case OneWay:
    int y = 123;
    FindYou(ref y);
    break;
  case TheOther:
    double y = 456.7; // illegal!
    GetchaGetcha(ref y);
    break;
}

You can solve this problem in a number of ways; the easiest is probably to wrap the body of the switch section in curly braces:

switch(x)
{
  case OneWay:
  {
    int y = 123;
    FindYou(ref y);
    break;
  }
  case TheOther:
  {
    double y = 456.7; // legal!
    GetchaGetcha(ref y);
    break;
  }
}

which tells the compiler "no, really, I want these to be different declaration spaces".

If you have a variable that you want to declare once and use it in a bunch of different places, that's legal, but a bit strange:

switch(x)
{
  case OneDay:
    string s;
    SeeYa(out s);
    break;
  case NextWeek:
    s = "hello"; // legal, we use the declaration above.
    Meetcha(ref s);
    break;
  }
}

That looks a bit weird, I agree, but it also looks a bit weird to have one switch block with two unbraced competing declarations in it. There are pros and cons of each, the design team had to pick one way or the other, and they chose to have switch cases not define a declaration space.

Case 4:

A funny consequence of the "reachability analysis" rules in the spec is that this program fragment is not legal:

int M(bool b)
{
  switch(b)
  {
    case true: return 1;
    case false: return 0;
  }
}

Of course in reality you would probably write this as the far more concise "return b ? 1 : 0;" but shouldn't that program be legal? It is not, because the reachability analyzer reasons as follows: since there is no "default" case, the switch might choose some option that is not either case. Therefore the end point of the switch is reachable, and therefore we have an int-returning method with a reachable code path that falls off the end of the method without returning. 

Yeah, the reachability analyzer is not very smart. It does not realize that there are only two possible control flows and that we've covered all of them with returns. And of course, if you switch on a byte and have cases for each of the 256 possibilities, again, we do not detect that the switch is exhaustive.

This shortcoming of the language design is silly, but frankly, we have higher priorities than fixing this silly case. If you find yourself in this unfortunate case, just stick a "default:" label onto one of the sections and you'll be fine.

-- Eric is on vacation; this posting was prerecorded. --

  • "You forgot to mention strange case no #5: You can not switch on type!"

    That's not strange at all, it follows from the basic rule that you can only have cases for compile-time constants, and that doesn't include type objects.

    Of course, that rule itself is rather annoying and it sure would be nice if we could have cases for arbitrary expressions...

  • Another argument for case 4 is the protection against hacked bools. ;)

    class Program

    {

      [StructLayout(LayoutKind.Explicit)]

      struct HackedBool

      {

         [FieldOffset(0)]

         int i;

         [FieldOffset(0)]

         bool b;

         public static bool IntToBool(int i)

         {

            HackedBool bh = new HackedBool();

            bh.i = i;

            return bh.b;

         }

      }

      static void Main(string[] args)

      {

         bool b = HackedBool.IntToBool(2);

         switch (b)

         {

            case false:

               Console.WriteLine("False");

               break;

            case true:

               Console.WriteLine("True");

               break;

            default:

               Console.WriteLine("FileNotFound");

               break;

         }

      }

    }

  • I generally prefer to use else if cascades instead of switch. I find it looks better in code, I can use wierd stuff in conditions, no break statements littering the flow. I would hope a good optimising compiler would produce much the same result anyway.

  • Shouldn't the third sentence of the first paragraph under the code example of "Case 2" read:

       It requires that every switch section, including the last one, have an *reachable* end point.

    It currently states "unreachable" instead of "reachable".

  • Why does it use the C syntax if it's not going to allow implied fall through anyway?

    Why not some new syntax? Say, case(1) { ... } case(2) { ... } like every other control structure ever.

  • @Jeff Yates: No.

    The end point can't be reached because there must be a break statement in the way.  So all end points are unreachable.

    Even without this, you couldn't insist that all end points be reachable; you might have an unconditional return.

  • Something I miss from C++ in the C# switch is the ability to declare a variable in the switch expression.

    It is not unusual to have to use the value we are switching on (especially so in the default case) and being able to store the value in a variable comes in handy.

    The workaround is pretty simple, but in order to avoid pollution of the scope with an additional variable, you have to add another block surrounding the switch. Compare the following:

    C++:

    switch (int nextValue = GetNextValue()) {

     case 0:

       return -1;

     ...

     default:

       return nextValue / 2;

    }

    C#:

    {

     int nextValue = GetNextValue();

     switch (nextValue) {

       case 0:

         return -1;

       ...

       default:

         return nextValue / 2;

     }  

    }

    The same applies to the while (even though that has a simpler workaround: you can always use a for).

    I'm not complaining or anything, but it would be interesting to know the reason why this limitation was introduced in the language (provided Eric reads this when he gets back from his vacations).

  • @My Twopence:

    I support your comment. There are some *core* functions missing.

    1) You can easily do a case-insensitive sort using Regex.Replace instead of String.Replace.

    I really miss two features regarding text in the framework:

    - being able to do accent-insensitive sorts and compares.

    - having a "natural" sort and comparer (sorting "file12" after "file2").

    2) Round has an overload with a number of decimal places to round to. It's a indeed strange that Truncate (and Ceil, Floor), didn't get the same option. It's easy enough to create your own helper method, though.

    3) At least the integer kind will be available in .NET 4 (BigInteger). Still missing an arbitrary precision real number, though.

  • Implicit fallthrough without break is not just a C/C++ thing - it also exists in Java. Given that Java is otherwise much more similar to C#, this could well be a subtle difference that can trip someone in a very nasty way. And between C, C++, and Java, we likely have the majority to be placated anyway, so people with other backgrounds will just have to accept it as an unavoidable legacy wart.

  • I vaguely remember in an early beta of C# (ten years ago?) that the switch had implicit breaks and they reverted to C style explicit breaks in a subsequent beta to avoid confusion. It seems a shame to me, but I was young at the time and didn't have any legacy affecting my judgement.

    @Anon: How does implicit breaks prevent reordering?

    @Tony Cox: In case b that you present, why would you expect to get fallthrough? Only beccause all other C-style languages (C, C++, Java, Javascript, etc) behave that way. I understand the reason, it just seems unfortunate that C# has repeat previous mistakes in the name of conformity.

    It seems that the switch statement was optimized for the rare case of fallthrough. Implicit breaks and explict fallthrough would be better suited for the common case, IMO.

  • I'm wondering how many programmers are actually using switches. Personally I prefer "if, else if" in almost all cases because I don't see there many advantages in using switches. "If" even reduces the indentation level, so why and when using switches? Any comments?

  • Multiple /Cascading if's being use in place of a switch statement mean a COMPLETELY different thing. The condition must be evaluated multiple times which can give rise to all sorts of issues.

  • One reason for me:

    Clarity.

    An if/elseif block can have a random other check within it. A Switch statement does what it says on the box and nothing else. I find it's much easier to read, organize, and understand, especially if you have, say, ten or more possibilities.

    Conversely, using a switch means you lose in adaptability -- I find that an acceptable compromise to being to tell what an entire block does at a glance, without having to check each term for an additional case.

    In some cases, a compiler might optimize a switch better than the associated if/elseif, but I wouldn't rely too heavily on that.

  • >There are pros and cons of each, the design team had to pick one way or the other, and they chose to have switch cases not define a declaration space.

    This makes sense, but why did they not take the third choice, to simply require writing a declaration space with angular brackets for each case?

    My ideal switch syntax would be like this:

    switch (foo)

    {

       case 1

       {

           Stuff();

           goto case 2;

       }

       case 2

       {

           if (OtherStuff())

               break;

           MoreStuff();

       }

    default

       MoreOtherStuff();

    }

    Every case its own declaration space, which is incredibly convenient. (Conversely, an entire switch being the same declaration space is rather useless, although I agree that without bracketed case labels, it's the proper choice.) We don't need any break statements anymore, because they're implicit in the closing brackets. (Of course we can still use them and case goto's.) We can also lose the colon.

    In the default case I left out the brackets like you might in an if-statement, because it's only a single line.

    The biggest advantage is probably that this syntax is far more consistent with the rest of C#. A list of statements that's logically seperated from the rest of the program is in its own bracketed block, except when it's only a single non-declaration statement, where you may leave out the brackets.

  • @Trevel: I agree completely!

    @Joren: In general (there are always exceptions to general rules), I absolutely despise that type of switch statement, it can quickly become nearly impossible to maintain. I strive to maintain a near topological equivilence between switch statements and (psuedo code):

    Dictionary<T, Func<T>> switch;

    switch[T](T);

    Over the long haul (e.g. a prgram that will be in active use/maintenance for a decade or more) simplicity and readability trump most other concenrns.

Page 2 of 3 (39 items) 123