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

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

  • If fall through is illegal, why is the break statement required to not fall through?

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

  • Two things I find particularly annoying about the C# switch statement:

    1) Having to enter break for every single case - since fall-through has to be explicitly declared (Case 1), why not just assume an implicit break if no explicit break, return etc. exists ?

    2) Not being able to specify a range e.g. case "A" to "Z", or case < 5, rather than having to explicitly declare every single case.

    Three other non-switch related gripes - is it unreasonable to expect a modern language to have:

    1) a built-in version of String.Replace that is case-insensitive ?

    2) a built-in version of Math.Truncate that allows you to specify decimal places (i.e. truncate the number to 2 decimal places WITHOUT rounding) ?

    3) a built-in arbitrary precision number type / library ?

    While LINQ, lambda expressions, delegates etc. are very cool, sometimes I find the lack of basic functionality exasperating.  

  • @My Twopence - explicit break is required as per re-ordering switch use-case justification in Case 2

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

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

  • "it doesn't make any sense to include a keyword that IMPLIES fall-through is possible, when it really isn't"

    I have to agree with this. I would also point out that we aren't forced to specify "return" at the end of a method that doesn't have a return parameter.

    I understand that the C# team thought they were making things easier for those with a C background, but at the same time they made things harder for those coming from other backgrounds (or no background). And honestly, once you have spent just a little time in the language and learn how it is different from C, the value of the explicit break is lost very quickly, while still being a constant annoyance.

  • In using Delphi (C# 0.1 as I like to call it) I got used to this idiom:

    case BooleanVar of

     True:

       Result := 1;

     else // False:

       Result := 0;

    end;

    This is the same as the C#:

    switch (booleanVar) {

     case true:

       return 1;

     default: // case false:

       return 0;

    }

    It's a habit I got into to avoid the compiler complaining about the Result not being set (a DailyWTF reader would assume it was allowing for TRUE, FALSE and FILE_NOT_FOUND...) but I see it will be useful for my future C# work as well.

    Incidentally, Delphi also has the ability to specify groups and ranges, as in:

    case C of

     'A','E','I','O','U': WriteLn('Vowel!');

     '0'..'9': WriteLn('Digit!');

     else WriteLn('Something Else!');

    end;

    Gnu C has a similar option.  It's a shame C# doesn't.

    (Oh, and: you don't have a preview button on this blog.  So if your comment formatter isn't smart enough to handle indentations and line breaks, this comment is going to be gibberish, which I'm afraid is your fault.  Blog commenting is a solved problem; failure to provide all the appropriate features is wilful folly.)

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

  • Theoretically, case 3 = bad compiler design to even allow that.  Consider:

    static void Main(string[] args)

    {

    string t = string.Empty;

    int i = 1;

    switch (i)

    {

    case 0:

    string s;

    s = "paco";

    t = s;

    break;

    case 1:

    s = "taco";

    t = s;

    break;

    }

    Console.WriteLine(t);

    Console.ReadKey();

    }

    This compiles AND works.  string s should be an unknown variable in case 1, but also since the program works, we see that s is allocated memory even though case 0 is never called to initialize it.  Wtf?

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

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

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

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

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

Page 1 of 3 (39 items) 123