Why can we only use constants in a switch-case statement?


Why can we only use constants in a switch-case statement? The following code fails to compile with the error “A constant value is needed” for someStr as it is not a constant string.

static void func(string str)
{
switch(str)
{
case “Zaphod”: Console.WriteLine(“The king”); break;
case someStr: Console.WriteLine(“The coder”); break;
default: Console.WriteLine(“None”); break;
}
}
string someStr = “Noo”;

Here goes a long answer to this short question.


The reason is simple and yet involved. Let’s take the following valid code which only has constants and see how it works.

static void func(string str)
{
switch(str)
{
case “Zaphod”: Console.WriteLine(“The king”); break;
case “Abhinab”: Console.WriteLine(“The coder”); break;
default: Console.WriteLine(“None”); break;
}
}

If we open see the code in IL it looks something like this

      L_0007: ldstr “Zaphod”
L_000c: call bool string::op_Equality(string, string)
L_0011: brtrue.s L_0022
L_0013: ldloc.0
L_0014: ldstr “Abhinab”
L_0019: call bool string::op_Equality(string, string)
L_001e: brtrue.s L_002f
L_0020: br.s L_003c
L_0022: ldstr “The king”
L_0027: call void [mscorlib]System.Console::WriteLine(string)
L_002c: nop
L_002d: br.s L_0049
L_002f: ldstr “The coder”
L_0034: call void [mscorlib]System.Console::WriteLine(string)
L_0039: nop
L_003a: br.s L_0049
L_003c: ldstr “None”

See the usage of op_Equality in L_000C and L_0019. This indicates that even though we are using switch-case, ultimately the code is converted to multiple if-then-else by the compiler. So the switch-case is converted by the compiler to something like

if (str == “Zaphod”)
Console.WriteLine(“The king”);
else if (str == “Abhinab”)
Console.WriteLine(“The coder”);
else
Console.WriteLine(“None”);

If this is the case then what is stopping the case statements from having non-constants? In case of a non-constant the code generated could be something like if (str == someNonConstantStr) which is valid code.


The answer is simple. When the numbers of cases are larger, the generated code is very different and is not constituted of if-then-else. Isn’t that obvious? Otherwise why would anyone ever use switch-case and why would we call switch case to be faster??


Lets see when we have a large number of case’s as follows what happens.

switch(str)
{
case “Zaphod”: Console.WriteLine(“The king”); break;
case “Abhinab”: Console.WriteLine(“The coder”); break;
case “Ford”: Console.WriteLine(“The Hitchhiker”); break;
case “Trilian”: Console.WriteLine(“The traveler”); break;
case “Marvin”: Console.WriteLine(“The Robot”); break;
case “Agrajag”: Console.WriteLine(“The Dead”); break;
default: Console.WriteLine(“None”); break;
}

For this first a class is generated by the compiler which looks like

Internal class CompilerGeneratedClass
{
internal static Dictionary CompilerGenDict;
}

And then for the switch case the following code is generated.

if (CompilerGeneratedClass.CompilerGenDict == null)
{
Dictionary dictionary1 = new Dictionary(6);
dictionary1.Add(“Zaphod”, 0);
dictionary1.Add(“Abhinab”, 1);

CompilerGeneratedClass.CompilerGenDict = dictionary1;
}
if (CompilerGeneratedClass.CompilerGenDict.TryGetValue(
str, out num1))
{
switch (num1)
{
case 0: Console.WriteLine(“The king”); return
case 1: Console.WriteLine(“The coder”); return;
case 2: Console.WriteLine(“The Hitchhiker”); return;

}
}
Console.WriteLine(“None”);

What this means is that first time the function is called a Dictionary of strings (key) and int (value) is created and all the cases are stored in this dictionary as the key and an integer as a value is stored against it. Then for the switch statement the string is taken and is queried in the dictionary and if it is present the number value for the string is returned. Using this number the compiler creates an efficient jump table and it jumps to the target Console.Writeline string.


Now the answer :). The strings are pre-stored in the dictionary. If the strings in the case statement were not constants, changes in them won’t reflect in the dictionary and hence you’d land up comparing against stale value. To avoid this inconsistency the non-constant values are not supported at all.


Obviously for dynamic values the dictionary cannot be used and hence there is no optimization possible for switch-case so one should anyway use if-then-else.

Comments (15)

  1. zproxy says:

    Actually the compiler may choose to use hash-table internally, so it will switch based on the hash of the switch argument. Then it will use the switch opcode.

  2. oidon says:

    The compiler could support variable string switches by exclusively using the if / else pattern for variable strings. Then, for constant string switches use either if / else or dictionary pattern as it presently does now. It seems pointless to restrict this scenario when it can so easily be overcome by manually using verbose if / else statements.

  3. zproxy says:

    Well, this isn’t a static switch then anymore.

    A static switch is a jump-table, which is fast:)

    You can have dynamic switches like this:

    using (var s = new Switch())

    {

    s["item1"] = delegate { /* action 1 */ };

    s["item2"] = delegate { /* action 2 */ };

    s[e.MyValues] = delegate { /* action 3 */ };

    }

    Ofcourse you have to implement it first, which is a no brainer.

  4. oidon: the point is that people use switch-case not for readability but with a mental perception of fast execution. So in case it allowed variable strings then some switches would be fast and others won’t be. Assume you are maintaining code, how would you go about figuring out which switch is of which format?? Won’t you be needed to right click on each and every case string and see whether it is const or not and iff all strings are const its of the faster type.

  5. oidon says:

    C#, like all languages, allows you do write efficient and inefficient code. If there is a major bottleneck, it will show up in a profiler. In this case, C# only mitigates the problem by forcing you to write verbose if / else statements instead of a concise switch statement.

    I prioritize readability over efficiency. Of course I then profile the code and rework troublesome parts for efficiency. Premature optimizations are often not worth it.

    If the switch syntax will not allow what I want, then I will rewrite it with if / else statements. It is all the same in the end, only the compiler gets in my way. I do not see any real merit in this imposed restriction.

  6. srivatsn says:

    I agree with Oidon. Since it results in no worse performance anyway, I should atleast be able to get the clear readability benefit from the switch/case.

  7. I always thought the decision to allow constants only is to increase readability and consistency. Lets assume you had a large switch case and you really needed fast execution here. Now this switch cases used only const strings. A new dev moves to your team and ignorant of this requirement adds another case which refers to a non-const string or changes one of the strings from const to non-const. Either way your performant switch degenerates to a slower if-else.

    There is no way to catch this easily either. Most people moving into C# from C/C++ and a lot of other languages use Switch with the believe that its faster than C. The use is not to promote readability but from the belief that switch executes faster. These people will also land up in trouble…

  8. eman says:

    After programming for almost two decades now, I must admit that I have never thought of (or even noticed) switch as being more efficient than if / else. Optimization details are left up to the compiler and vary greatly. My main responsibility is to write clear, bug-free code. Others will read my code. And I need to read others code. Readability is important. As oidon mentioned, a profiler will indicate areas that need more work. It is not something that I need to "catch" or second-guess without evidence to the contrary.

    Besides, you can’t switch on a string in C/C++, so that argument is hardly relevant.

    Without other evidence to the contrary, I am inclined to think that this part of C# was poorly designed.

  9. JonInNC says:

    It has nothing to do with optimization. Allowing non-constant case expressions would lead to very unclear code at worst, and confusing rules to remember at best. What is the behavior supposed to be if multiple cases use variable expressions and two of those expressions happen to have the same value?

       string caseStr1 = "Apple";

       string caseStr2 = "Apple";

       switch(str)

       {

           case caseStr1: Console.WriteLine("The king"); break;

           case caseStr2: Console.WriteLine("The coder"); break;

           default: Console.WriteLine("None"); break;

       }

    Should both blocks get executed? Should the first? If you allow overlapped conditions for non constant expressions, you have to allow them for constant expressions. One of the primary goals of the designers of C# was to encourage code that was easy to understand(which is why there are no macro black magic, and no typedefs). Allowing non-constant expressions would greatly complicate a simple construct without adding any value(There’s nothing you cant already do by putting an if statement or two before the switch/case)

    My guess is that the reason that the compiler generates if/else blocks for small switches is that it it probably is faster to perform a few if statements that set up the hash.

    To really get the behavior that you want, zproxy’s  methods is great. (nice C# 3.0 reference, by the way zprox)

    You should be aware that, depending on what you do, a delegate call could be a lot slower. Ignoring performance totally until all the code is written is just as bad as premature op. And in cases like middleware code, you don’t have the luxury of knowing exactly how your code will get abused :)

  10. eman, oidon why do you think C# is badly designed here. C, C++ behaves exactly the same way even when it switches on int via jump  tables. Both need literals here.

  11. oidon says:

    @abhinaba

    While the C# (and C / C++) syntax disallows it, it can be and often is worked around by rewriting as if / else if… Thus, the syntax is not really solving a problem but only getting in the way.

    @JonInNC

    I view switch statements as shorthand for if / else if… statement. Thus, per your example, I would expect it to execute only the first match (case 1).

    Honestly, I do not expect C# to change. However, I will continue to workaround this restriction with if / else if statements when necessary. That is why I feel it is counter productive.

  12. MZ says:

    Sad.  Too bad C# can’t have any way to write compile-time code (preprocessor macros, functional eager calculation, etc.)  

    In languages where you could do that, "const" would be less of a problem… still, either way this means that you can never use a linebreak in switchable text if your code is cross-platform, since environment.newline is evaluated at run-time and thus is unusable for this purpose.

  13. zproxy says:

    Actually now to come to think about it, one could precompile my example to a single function of bytecode.

    See the class System.Reflection.Emit.DynamicMethod

    A switch block has the ability of a fall thro via goto next case, which the if/else does not have.

    By precompiling my example there will be no significant overhead.

    I think this will work in a full trusted evironment only.

    :)

  14. David says:

    [quote JonInNC]

    One of the primary goals of the designers of C# was to encourage code that was easy to understand(which is why there are no macro black magic, and no typedefs).

    [/quote]

    The typedef keyword is gone, but type aliasing is still available with the "using" keyword. See http://msdn2.microsoft.com/en-us/library/sf0df423.aspx.

    I don’t know why you think removing type aliasing would make code easier to understand. Especially when using generics, type aliasing makes code much more readable.