Share via


Four switch oddities

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

Comments

  • Anonymous
    August 13, 2009
    I often need to use braces under case statements (Case 3 above) in both C# and C++.  But the IDE insists on indenting the braces, so the code under the case is double indented.  Which has nothing to do with the language design, but is a constant irritant.

  • Anonymous
    August 13, 2009
    If fall through is illegal, why is the break statement required to not fall through?

  • Anonymous
    August 13, 2009
    @Random832: Are you suggesting that there should be an implied break? I think that would be potentially confusing - you might think you're getting fall-through (especially if you come from a C/C++ background), but instead you're getting a break, with no indication from the compiler that you're might not be getting what you intended. With the way it works now, there's no possiblity for confusion. Basically, you want to make it so that if it compiles succesfully, there's the maximum possible chance that you're getting the expected behavior.

  • Anonymous
    August 13, 2009
    The comment has been removed

  • Anonymous
    August 13, 2009
    The comment has been removed

  • Anonymous
    August 13, 2009
    "With the way it works now, there's no possiblity for confusion." -- obviously there is or he wouldn't have had to make this post. And as someone who has used various flavours of Basic, there's no confusion in not having breaks and IS in having them; why am I putting in something to say not to do something that I can't do in the first place? If the Break statement is mandatory, why not just leave it out? I can understand why they were left in -- approaching from a C perspective where fall-through is assumed, it makes sense to say explicitly that that isn't happening. Approaching from a Basic perspective, where fall-through does not occur, it doesn't make any sense to include a keyword that IMPLIES fall-through is possible, when it really isn't. In other words, from my perspective, the word "break" is only there as a trap for the unwary. It doesn't do anything but break your build if you leave it out.

  • Anonymous
    August 13, 2009
    The comment has been removed

  • Anonymous
    August 13, 2009
    The comment has been removed

  • Anonymous
    August 13, 2009
    The decision about whether to have implicit breaks or to require them explicitly boils down to which problem you think is worse: a) You omit a break that was required, and your code doesn't compile b) You expected to get fallthrough, but instead you got a break I submit that the first problem is a better one to have. It doesn't compile, so you immediately know you did it wrong, and it's also pretty obvious from the error message how to fix it. With the second problem, you could quite happily check-in code which compiles just fine, and which has a bug in it which you will only discover later when you run test cases which exercise those code paths.

  • Anonymous
    August 13, 2009
    The comment has been removed

  • Anonymous
    August 13, 2009
    I'm surprised the one legal fall-through case was not mentioned above:            int i = 0;            switch (i)            {                case 0:                case 1:                    // legal!                    break;                case 2:                    break;                default:                    break;            } I'm assuming the grouped case statements are considered one section which is why this works.

  • Anonymous
    August 13, 2009
    I don't know whether it's sad or fortunate that C# will never know the brilliance of Duff's Device.

  • Anonymous
    August 13, 2009
    @David. Saying because there's a break statement there's an implication of fall-thought is a little strong. Maybe all that's needed is a better compiler message "ERROR: All cases require break or return, even default. Fall-through is not permitted." to clear up the confusion. Like it or not, C# comes from a C heritage, and while it's anachronistic to some, especially 10 years or so later, I find the explicitness of switch better than new implied rules that contradict old habits. The fact that it never falls through is a new feature to the old switch statement, and is always required mainly because so many stupid bugs occurred in the past because the 'implied' behavior was easily missed, even with  code reviews.

  • Anonymous
    August 13, 2009
    Justifiable as the current approach is, it does mean that to anyone learning C# who has never learned C or C++ (and the proportion of learners for whom this is true is ever-increasing, I would suggest), your language looks a little bit stupid.

  • Anonymous
    August 13, 2009
    c# switch is an awful mess. I'd like an updated switch in c# 4 that takes a lambda for its case statements. You forgot to mention strange case no #5: You can not switch on type! Oh, and in case #2 you say "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". Fine, but doesn't case 3 stop you from reordering your switch statements? To really be able to reorder arbitrarily, case needs its own declaration space.

  • Anonymous
    August 13, 2009
    "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...

  • Anonymous
    August 13, 2009
    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;      }   } }

  • Anonymous
    August 13, 2009
    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.

  • Anonymous
    August 14, 2009
    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".

  • Anonymous
    August 14, 2009
    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.

  • Anonymous
    August 14, 2009
    @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.

  • Anonymous
    August 14, 2009
    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).

  • Anonymous
    August 14, 2009
    @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").
  1. 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.
  2. At least the integer kind will be available in .NET 4 (BigInteger). Still missing an arbitrary precision real number, though.
  • Anonymous
    August 14, 2009
    The comment has been removed

  • Anonymous
    August 14, 2009
    The comment has been removed

  • Anonymous
    August 15, 2009
    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?

  • Anonymous
    August 15, 2009
    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.

  • Anonymous
    August 15, 2009
    The comment has been removed

  • Anonymous
    August 15, 2009
    The comment has been removed

  • Anonymous
    August 16, 2009
    @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; switchT; 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.

  • Anonymous
    August 16, 2009
    @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?

  • Anonymous
    August 16, 2009
    The comment has been removed

  • Anonymous
    August 16, 2009
    The comment has been removed

  • Anonymous
    August 16, 2009
    @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.

  • Anonymous
    August 17, 2009
    @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...)

  • Anonymous
    August 20, 2009
    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.

  • Anonymous
    August 24, 2009
    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:)

  • Anonymous
    July 22, 2010
    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 

  • Anonymous
    April 12, 2012
    The final code block in Case 3 has an extra closing brace.