Andere Möglichkeit des Vergleichs: switch

Als Programmierer offenbart sich die Switch-Anweisung vorrangig als leserlichere Form vieler verschachtelter if-Abfragen.

Das Problem

Schauen wir uns den folgenden Programmabschnitt an, in dem es vier Möglichkeiten gibt, wie das Programm in Abhängigkeit von Wert weiter verfahren soll:

if ( command == 1 )
{
  /* Programm, wenn command 1 ist */
}
else
{
  if ( command == 2 )
  {
    /* Programm, wenn command 2 ist */
  }
  else
  {
    if ( command == 3 )
    {
      /* Programm, wenn command 3 ist */
    }
    else
    {
      /* Programm, wenn command nicht 1 oder 2 oder 3 ist */
    }
  }
}

Nun stelle man sich vor, es gäbe nicht nur vier Möglichkeiten, sondern 10 oder 20 oder 100. Um richtig zu verschachteln, müsste man immer weiter nach rechts gehen und irgendwann wäre der Bildschirm zu klein. Das ist also bei sehr großen Listen unhandlich, aber derartig große Unterscheidungen sind in der strukturierten C-Programmierung nicht ungewöhnlich (In der objektorientierten Programmierung haben wir hier noch ein zusätzliches Werkzeug: die virtuellen Methoden). Um die Lesbarkeit und Geschwindigkeit zu erhöhen, gibt es in C neben if auch das switch-Konstrukt.

Vorbedingungen

Um switch verwenden zu können müssen zwei Bedingungen erfüllt sein:

  1. es darf nur um einen (1) Vergleich auf Gleichheit (==) handeln. Eine Kombination ( a == 4 && b == 3 ) geht genausowenig, wie Bereichsüberprüfungen ( a >= 3 ).
  2. der Vergleich muss mit einer konstanten Identität sein.

Zu den Bedingungen gibt es keine Ausnahmen, aber man kann mit den Bedingungen etwas jonglieren, so dass sich entweder der Vergleich auf Ungleichheit ( a != 4 ) hinbiegen lässt oder Kombinationen von Oder-Verknüpfungen ( a == 3 || a == 4 ).

Was ist eine "konstante Identität"

Fangen wir mit dem einfachsten Begriff an: konstant bedeutet, dass ein Wert sich nicht ändern darf. Ein Vergleich zu einer Variablen ist damit ausgeschlossen. Der Variablen a könnte man zur Laufzeit einen anderen Wert zuweisen, wohingegen 4 egal - was passiert - seinen Wert behalten wird.

Die Identität ist ein etwas abstrakter Begriff in der Informatik, gelegentlich wird hier auch der Begriff „Token“ oder „Key“ verwendet.

Es bedeutet, dass ein Objekt eine eindeutige Identifizierung besitzt, die sich niemals ändert. Die Identität besitzt damit eine eindeutige und unveränderliche Bedeutung. Selbst bei Objekten, die identische Daten enthalten, muss sich die Identität unterscheiden. Das ist bei Zahlen vergleichsweise einfach, denn die Identität entspricht dem Wert der Zahl. Schließlich gibt es keine zwei Zahlen, die den gleichen Wert besitzen, also gibt es auch keine zwei Zahlen, die dieselbe Identität besitzen. Hat man zwei Strings

char str1[6] = "Hallo";
char str2[6] = "Hallo";

so sind sie gleich, weil in beiden „Hallo“ steht, sie besitzen aber nicht die gleiche Identität. Bei Zeigerobjekten (hier: char *) entspricht die Adresse, an der die Daten gespeichert sind, der Identität. Wenn das erste 'H' von str1 an Adresse 1000 liegt, und das 'H' von str2 an Adresse 2000, so sind die Strings gleich (denn sie gleichen sich), aber es sind nicht dieselben: Sie haben eine unterschiedliche Identität, sie sind nicht identisch. Die eine Identität ist 1000, die andere 2000. Wie bei einem Zwillingspärchen, das sich augenscheinlich zwar gleicht, aber nicht den identischen Personalausweis besitzt.

Da C nur Identitäten vergleicht, kann man C-Strings auch nicht mit == sinnvoll vergleichen, sondern muss die Funktion strcmp() nehmen, die Buchstabe für Buchstabe vergleicht.

Nun könnte man sagen, dass Strings ja eine konstante Identität haben würden, wenn man immer nur die Adresse von einem String verwenden würde. Dem ist leider auch nicht so, denn jedes Mal wenn das Programm gestartet wird, stehen die Strings an einer anderen Adresse im Speicher. Sie sind also nicht vollkommen konstant, obwohl das für switch erforderlich ist.

Neben Zahlen, haben in C enums konstante Identitäten.

Warum müssen Identitäten konstant sein?

Die Idee hinter switch ist nicht ausschließlich die Lesbarkeit für den Programmierer zu erhöhen, sondern auch das Programm zu beschleunigen. Bessere Compiler emittieren (Befehle für den Prozessor schreiben) daher keine if-Konstruktion, die solange fragt, bis der richtige Wert gefunden wurde. Aus allen möglichen Werten, wird vom Compiler ein passender Algorithmus gewählt, der mit möglichst wenig Aufwand schnell den richtigen Wert findet. Das können Hashtables sein, eine binäre Suche, das ist dem Compilerentwickler überlassen. Da die Werte zur Wahl des optimalen Algorithmus feststehen müssen, müssen die Werte absolut fest stehen und auch keine Adressen sein, die sich zur Laufzeit nicht ändern, aber eben noch nicht feststehen, wenn das Programm geladen wird.

Wenn man also viele Vergleiche tätigen muss, und kann die oben benannten Bedingungen einhalten, ist die switch()-Anweisung vorzuziehen, da sie schneller sein kann.

Die switch-Syntax

Das obige Programm kann wie folgt mit switch dargestellt werden:

switch( command )
{
  case 1:
  {
    /* Programm, wenn Wert 1 ist */
    break;
  }
  case 2:
  {
    /* Programm, wenn Wert 2 ist */
    break;
   }
  case 3:
  {
    /* Programm, wenn Wert 3 ist */
    break;
  }
  ...
  default:
  {
    /* Programm, wenn Wert nicht 1 oder 2 oder 3 */
    break;
  }
}

So wird das Programm zunächst einmal deutlich übersichtlicher. In die erste Zeile wird die zu vergleichende Variable eingebettet, schließlich beginnt der Anweisungsblock ('{' und '}'), in dem die eigentlichen Abfragen stattfinden. Dies geschieht mit dem Schlüsselwort case. Hinter case muss die Identität stehen, die mit der in switch angegebenen Variable verglichen wird. Hinter case beginnt ein weiterer Anweisungsblock. Dort wird das Programm fortgeführt, wenn die Variable die Identität enthält, die hinter case angegeben wurde.

Jeder Anweisungsblock sollte mit der break-Anweisung enden. Sie bewirkt, dass die switch-Anweisung verlassen wird und springt an die Stelle, die die geschlossene geschweifte Klammer (}) des switch-Blocks markiert. Vergisst man das break, so läuft das Programm in die nächste case-Anweisung. Das ist häufig eine Fehlerursache, da der nächste Case häufig die Arbeit des gewollten Case wieder kaputt macht. Daher ist ein vergessenes break eine beliebte Fehlerursache.

Allerdings werden dadurch auch solche Konstrukte möglich, die einer kombinierten Oder-Abfrage ( Wert == 1 || Wert == 2) entspricht:

switch( command )
{
  case 1: case 2:
  {
    /* Programm, wenn Wert 1 oder 2 ist */
    break;
  }
  case 3:
  {
    /* Programm, wenn Wert 3 ist */
    break;
  }
  default:
  {
    /* Programm, wenn Wert nicht 1 oder 2 oder 3 */
    break;
  }
}

Der Anweisungsblock der hinter default liegt wird immer dann ausgeführt, wenn das Programm bei default ankommt. Das passiert, wenn für den Wert der Variablen kein case existiert. Der Wert könnte zum Beispiel 5 sein, dafür existiert keine case-Anweisung. Wird break vergessen (oder weggelassen) so wird der Anweisungsblock hinter default ebenfalls ausgeführt, sofern keine break-Anweisung anderswo dazwischen liegt.

Enums benutzen statt MagicNumbers

MagicNumbers werden in der Informatik Zahlen genannt, die irgendeine „magische“ Bedeutung haben.
Leider weiß man in der Regel nicht welche.

Daher ist es ein guter Brauch, statt Zahlen enums zu verwenden. Das sind im Prinzip nichts anderes als Zahlen, sie besitzen daher ebenfalls eine konstante Identität, doch statt der Zahlen mit unbekannter, magischer Bedeutung, kann man enums einen richtigen Namen geben. Dieser Name sollte natürlich dem Programmierer etwas sagen, so dass er das Programm und dessen Aufgabe besser verstehen kann.

#include <stdio.h>
#include <stdlib.h>
 
enum TrafficLightState
{
  TLSTATE_OFF,
  TLSTATE_GREEN,
  TLSTATE_YELLOW,
  TLSTATE_RED,
  TLSTATE_RED_AND_YELLOW
};
 
enum TrafficLightState state = RED;
 
int main( void )
{
  switch (state)
  {
    case TLSTATE_RED: case TLSTATE_RED_AND_YELLOW:
    {
      printf( "Du musst vor der Ampel warten.\n" );
      break;
    }
    case TLSTATE_YELLOW:
    {
      printf( "Du musst vor der Ampel anhalten.\n" );
      break;
    }
    case TLSTATE_GREEN:
    {
      printf( "Du darfst über die Ampel fahren.\n" );
      break;
    }
    default:
    {
      printf( "Die Ampel zeigt nichts sinnvolles an. Bitte beachte das Schild an der Ampel.\n" );
      break;
    }
  }
 
  return EXIT_SUCCESS;
}

Diskussionsthread