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. --

  • @TheCPUWizard:

    Are you talking about jumping from case to case by goto? If so, I dislike that as well, but my post was really only about syntax. The content of the cases was just to give some more color to the example.

    If not, could you please explain what you mean?

  • I find it interesting the proposed reasoning behind point 2 (that explicit breaks are required even in "default:"--that arbitrarily re-ordering switch sections won't introduce a breaking change--is completely violated by the syntax in point 3; clearly declaring a variable in one section to be used in subsequent sections inherently is un-re-orderable.

  • @Joren: The goto threw it over the top, but even the internal branches within a case give me cause for concern in terms of maintainability. In most cases I try to organize a switch statement so that each case is one of the following:

    a) A set of assignments to a group of variables

    b) A set of calls to a group of methods with the method being determined by the value

    c) A call to a method with differing parameters based on the value

    @Peter. I believe the difference is that the effect of allowing fall-through [case #2] is that it would be a COMPILABLE (and therefore runnable) breaking change in program execution, while the latter [case #3] would generate a syntax error, and thus it would just be a compilation error that was easily detectable, and would not result in a runnable program which behaved differently...we will see what Eric has to say when he returns.....

  • @Mark Implicit breaks don't prevent re-ordering. But you're just trading one inconsistent behavior for another for the sake of 5 extra characters and some explicitness. Obviously there's a bit of contention on the issue, so explicitness is probably just erring on the side of caution. You can't please everyone. Personally I just don't care.

    ---

    Maybe you're all right, they should probably have called it something else than switch,  started from the ground up, and made it do what you want, and not just be a hangover from C++, but when it gets down to needing something fancier, you have the tools to write your own code solution, so the case for the ultraswitch class is lacking that traction.

    I've already made a generic class that takes an expression to evaluate, and executes a delegate to achieve the results. You get to do all the things you talk about (no goto or fall-through), and it's pretty simple to code. To be sure, it was a lot harder before there were generics, but now the class is almost trivially simple.

    Sometimes it's more important to give you the tools for you to create code that can get the job done (i.e. generics), versus bloating the core language with every possible feature that, in the end, doesn't really get you that much farther for all the effort.

    @Larry Lard

    Switch is pretty basic, but it basically works, and works well. If you don't like it, don't use it I guess. It's not something to get too upset over. I'm no defender of the faith, but calling a language stupid because it does something different, or has some legacy n00b's don't get is, well stupid. All languages have their own baggage of inconsistent stupid things, but as I'm in no position to change them, and can't be bothered to write my own language (there are already too many IMHO) I just have a cup of tea instead.

  • @CPUWizard  I mean, if the principle of no fall-through is to facilitate this:

    switch(x)

    {

     case 1:

      DoSomething();

      break;

     case 2:

       DoSomethingElse();

       break;

    }

    ... being reorganized to:

    switch(x)

    {

     case 2:

       DoSomethingElse();

       break;

     case 1:

      DoSomething();

      break;

    }

    (which is a laudable principle)  then why that principle didn't seem to proliferate to the design of other syntax; notably variable declarations in switch scope.  For example:

    switch(x)

    {

     case 1:

      String text = "1234";

      DoSomething(text);

      Console.WriteLine(text);

      break;

     case 2:

       text = "4567";

       DoSomethingElse(text);

      Console.WriteLine(text);

       break;

    }

    ... now breaks with an identical re-ogranization:

    switch(x)

    {

     case 2:

       text = "4567";

       DoSomethingElse(text);

      Console.WriteLine(text);

       break;

     case 1:

      String text = "1234";

      DoSomething(text);

      Console.WriteLine(text);

      break;

    }

    Sure, one avoids a logic error and the other doesn't avoid a compile error; but, I just find it curious that principles cited for reasons /why/ a syntax is the way it is don't seem to influence other syntax.  i.e. the principle didn't seem to proliferate through design process for the rest of the syntax (or, we haven't been made aware of whether or not a concession was made for this particular syntax and why).  Or, it wasn't an influencing principle at all; just a side-effect of the design which is now used to justify the syntax (which is neither bad nor good; just possible fact).  This is no "answer" to this; we know why it does it this way (the spec says so, that's the way it was implemented, no one has changed it, more intuitive coming from C/C++, and maybe to be similar with C/C++, etc...)

  • I like the explicitly-required break or goto. I'm new to C# but I love it. I've made thousands of run-time errors in many other languanges, :-(

    I prefer compile-time errors over errors that are missed by code inspection or testing, but get found by customers. Not that I've ever done that.

  • this case REALLY looks weird:

    <pre>switch(x)

    {

     case OneDay:

       string s;

       SeeYa(out s);

       break;

     case NextWeek:

       s = "hello"; // legal, we use the declaration above.

       Meetcha(ref s);

       break;

     }

    }</pre>

    due to the spurious end-brace at the end:)

  • Case 2, first paragraph after code sample, 3rd sentence reads "It requires that every switch section, including the last one, have an unreachable end point."

    Don't you mean "reachable end point"?

    No, I mean "unreachable end point". Consider "break;", or "return;" or "goto case default;". In each of those, you "never get to the semicolon", so to speak. The end point of each of those statements is unreachable; the code that immediately follows the semicolon is not the thing that runs next. Compare that to, "M();" - now the semicolon and whatever comes after is reachable, after M() completes. - Eric 

  • The final code block in Case 3 has an extra closing brace.

Page 3 of 3 (39 items) 123