Switch Statement: All you ever wanted to know, and then some.
Switch Statements
X++, like most all imperative languages, has the concept of a switch statement. Typically, it is used in the cases where a range of things must happen based on the value of an expression that is calculated as part of the switch statement. As it happens, there is some differences in the way that switch statements work among contemporary programming languages. I will look at the X++ version here and contrast it with what you may know from C# and other languages. I will take the opportunity to show some bugs that has caused issues for us recently and provide guidance on some fixes we are making.
So, the task of the switch statement is clear: Branch out to code depending on the value of some expression. However, each language has its own set of restrictions that it imposes to make it feel natural inside the language where it is defined, and often to make runtime evaluation as efficient as possible. X++ was designed at the time where C++ and Java were en vogue, and it shows in the X++ implementation of the switch statements. The X++ switch statements are what is known as unstructured switch statements, as opposed to the structured switch statements in Pascal, for instance. This means that the expectations of developers who are used to the more modern structured switch statements may not know of the differences between the two.
Let’s start off with two definitions:
Unstructured switch statements
An unstructured switch statement is one where code execution will continue into the next case (without evaluating the subsequent case values), unless the flow is explicitly changed by the developer, often using the a break statement. In other words:
switch (1)
{
case 1: print 1;
case 2: print 2;
default: print 3;
}
will print 1,2,3. This may come as a surprise to those that expected a Pascal like structured switch. Here the switch statement can be seen as a generalized if-then-else conditional with any number of branches. Interestingly the flow-over semantics cannot be expressed in any other way in X++. Its implementation relies on having an unstructured GOTO facility that X++ lacks.
Structured switch statements
A structured switch statement is the kind that is offered in programming languages like the Algol family. I will illustrate with a Pascal snippet:
case (1) of 1: writeln(1); 2: writeln(2); otherwise writeln(3); end;
The execution of this pascal statement will simply output 1; each case will transfer to the statement after the switch statement after execution. For all intents and purposes, structured switch statements are syntactic sugar applied to simple if ... else statements. However, there are many interesting ways to generate code for these, that are very effective, based on dictionaries.
X++ semantics
I will describe the semantics of the X++ unstructured switch statement by explaining the code that it generates. Consider:
switch (e0)
{
case e1: S1…
case e2: S2…
…
case en: Sn…
default: Sd;
}
in which the ei are expressions and the Si... are statement lists. The code generated will look like (in suitable pseudo code):
// Calculate the switch expression once, to avoid possible extra side
// effects and poor performance.
var $temp = e0;
if ($temp == e1)
goto label 1;
else if ($temp == e2)
goto label 2;
else if … // for all other case selector values.
…
else if ($temp == en)
goto label n;
// default part.
goto label d;
label 1:
S1…
label 2:
S2…
label …
…
label n:
Sn…
label d: // The default part
Sd…
label AfterSwitch:
As you can see, once the if statements have determined what label to go to, the execution will just continue to evaluate the statements in the cases that follow. If this is not what you want (and I am sure it is not), then you need to explicitly break the flow of execution. In X++ this can happen using the break, continue, retry, return and throw statements. If a break statement is used, the switch statement itself is the outer boundary that the break will exit to – In other words, a break statement inside one of the statements will go to label AfterSwitch.
By the way, the fact that you flow from one case to the next is why the following does what you might expect:
switch (expr)
{
case 0:
case 1: print 'zero or one';
case 2: print 'two';
default: print 'three';
}
If the expression has the value 0, then the empty statement is executed and, since there is nothing to modify the flow of control, execution will fall into the 1 case. So zero and one will both execute the same print statement. There is a more compact way to specify a list of eligible values:
switch (expr)
{
case 0, 1 : print “zero or one”;
case 2: print “two”;
default: print “three”;
}
The point is, you have to manage the flow on your own in X++. C# borrows the syntax from C but will not allow the code in a switch to flow from one case into another, except for the empty case described above.
Case values are not necessarily constants
The case values provided in the examples above have all been constants, and that is certainly the most common way to use switch statement. However, the switch statement can be used in a different way: for checking multiple expressions against one value rather than one expression against many values. Here the question is: Which variable has the value 5:
We do not have many cases of this in our codebase. This is what one case looks like:
switch (true)
{
case caLLerIsReport && !callerIsReportAuto:
…
break;
case callerIsForm:
…
break;
case (sysQueryRun.query().name() && sysQueryRun.query().name() != #queryNameTemporary):
…
break;
default:
…
break;
}
While there is certainly nothing functionally wrong with this construct, I think that the author would have been better off writing this as normal if statements.
Trouble by default
Was we have seen above it is possible (but not mandatory) to provide a default part to a switch statement. As you know, it is basically an else part that gets executed when none of the hitherto tested case values match. However today there are some problems with default parts that can possibly cause errors in your code.
Multiple defaults
Consider this perfectly legal example:
switch (this.expr())
{
case 1: print "Case 1"; break;
case 2: print "Case 2"; break;
default: print "first default";
case 3: print "Case 3"; break;
default: print "default case";
}
When the switch expression evaluates to 3 you will not get to the case 3 entry, but into the default case. There are no good semantics that can be applied to this case, so we are fixing this case in the compiler in the very near future. You will no longer be able to provide more than one default case, since it is nonsensical. There is a Socratex query to find such examples in your code base (it is included as SwithDuplicateDefault.xq in the samples on github. Refer to the blog items about Socratex to understand how to apply the queries and how to consume the results).
Default not the last entry
In this case there is exactly one default part but it is not the last one, as shown in this currently perfectly valid snippet:
switch (i)
{
case 1: print 'Case 1'; break;
default:
case 2: print print 'Case 2 or default'; break;
case 3: print 'Case 3';
}
The semantics in other languages that support this (notably C++, but also Java, Javascript and others) is that the entry where “case 2 or default” is printed is executed if either (i == 2) or I does not match any other case, i.e. if i has the value 45. If (i == 3), then obviously that case should be executed. Here is a table that describes the expected and actual values:
i |
Expected printout (C++ et al) |
Actual (X++) |
1 |
"Case 1” |
"Case 1” |
2 |
"Case 2 or default"; |
"Case 2 or default" |
3 |
"Case 3" |
"Case 2 or default" |
4.. |
"Case 2 or default" |
"Case 2 or default" |
Again, there is a Socratex query to find such cases (SwitchDefaultNotFinal.xq). Unfortunately we cannot change the runtime to make X++ match the expectation from Java or C++ since it would change the behavior of existing code. Instead we will soon make the compiler diagnose the situation and ask the developer to put the default part last.
Declarations and usage in case statements
This is very subtle, and it translates into subtle bugs. Even so we have diagnosed several of these cases in our code base. Let us consider this snippet:
switch (i)
{
case 0;
// i is not visible here.
break;
case 1:
int i; // i is declared here
i = 1;
break;
case 2:
print i; // This is the i declared in the case above.
break;
...
}
The issue here is caused by the fact that you can declare variables in an imaginary scope that is the switch statement itself. Since i is declared inline it is visible inside all the cases following its declaration. In the case where the expression evaluates to 2, the value will not have been initialized and has the default value. The old adage holds once again: Make your scopes as small as possible:
switch (e)
{
case 0;
// i is not visible here.
case 1:
{ // Compound statement, scoping i
int i;
i = 1;
break;
}
case 2:
print i; // Error: I is not in scope.
...
}
That's it for now. Enjoy coding!
Comments
-
Yes, I am a firm believer in making scopes as small as possible. You would have to use the scope if you were using an if statement to accomplish the same semantics. Sometimes I find myself doing: switch { case 1: { ... } break; case 2: ... }
-
Hi Peter, So you recommend using compound statements inside the cases to scope variables to the case? I already thought about it but it „felt“ wrong.
*This post is locked for comments