2.6 Kontrollstrukturen
Es gibt sicherlich kein Programm, das ohne die Steuerung des Programmablaufs zur Laufzeit auskommt. Das Programm muss Entscheidungen treffen, die vom aktuellen Zustand oder von den Benutzereingaben abhängen. Jede Programmiersprache kennt daher Kontrollstrukturen, um den Programmablauf der aktuellen Situation angepasst zu steuern. In diesem Abschnitt werden Sie die Möglichkeiten kennenlernen, die Sie unter C# nutzen können.
2.6.1 Die »if«-Anweisung
Die if-Anweisung bietet sich an, wenn bestimmte Programmteile nur beim Auftreten einer bestimmten Bedingung ausgeführt werden sollen. Betrachten wir dazu das folgende Beispiel:
static void Main(string[] args) {
Console.Write("Geben Sie Ihren Namen ein: ");
string name = Console.ReadLine();
if(name == "")
Console.WriteLine("Haben Sie keinen Namen?");
else
Console.WriteLine("Ihr Name ist \'{0}\'",name);
Console.ReadLine();
}
Listing 2.20 Einfache »if«-Anweisung
Das Programm fordert den Anwender dazu auf, seinen Namen einzugeben. Die Benutzereingabe wird von der Methode ReadLine der Klasse Console entgegengenommen und als Rückgabewert des Aufrufs der Variablen name zugewiesen. Um sicherzustellen, dass der Anwender überhaupt eine Eingabe vorgenommen hat, die aus mindestens einem Zeichen besteht, wird der Inhalt der Stringvariablen name mit
if (name == "")
überprüft. Wenn name einen Leerstring enthält, wird an der Konsole
Haben Sie keinen Namen?
ausgegeben. Beachten Sie, dass die zu prüfende Bedingung hinter dem Schlüsselwort if grundsätzlich immer einen booleschen Wert, also true oder false, zurückliefert. Hat der Anwender eine Eingabe gemacht, wird die Eingabe mit einem entsprechenden Begleittext an der Konsole ausgegeben.
Das Kernkonstrukt der Überprüfung ist die if-Struktur, deren einfachste Variante wie folgt beschrieben wird:
if (Bedingung)
[...]
[else
[...]
Die if-Anweisung dient dazu, in Abhängigkeit von der Bedingung entweder die Anweisung1 oder die Anweisung2 auszuführen. Ist die Bedingung wahr, wird die Anweisung1 ausgeführt, ansonsten die Anweisung2 hinter dem else-Zweig – falls ein solcher angegeben ist, denn der else-Zweig ist optional.
Beachten Sie, dass es sich bei der Bedingung in jedem Fall um einen booleschen Ausdruck handelt. Diese Anmerkung ist wichtig, denn wenn Sie bereits mit einer anderen Programmiersprache wie beispielsweise C/C++ gearbeitet haben, werden Sie wahrscheinlich zum Testen einer Bedingung einen von 0 verschiedenen Wert benutzt haben. In C# funktioniert das nicht! Nehmen wir an, Sie möchten feststellen, ob eine Zeichenfolge leer ist, dann müssten Sie die Bedingung wie folgt definieren:
// Deklaration und Initialisierung der Variablen myText
string text = "";
[...]
if(0 != text.Length)
Console.Write("Inhalt der Variablen = {0}", text);
Length liefert, wenn sie auf die Variable einer Zeichenfolge aufgerufen wird, die Anzahl der Zeichen zurück.
Da es in C# keine Standardkonvertierung von einem int in einen bool gibt, wäre es falsch, die Bedingung folgendermaßen zu formulieren:
// ACHTUNG: In C# nicht zulässig
if (text.Length)...
In einer if-Bedingung können Sie beliebige Vergleichsoperatoren einsetzen, auch in Kombination mit den logischen Operatoren. Das kann zu verhältnismäßig komplexen Ausdrücken führen, beispielsweise:
if (a <= b && c != 0)...
if ((a > b && c < d)||(e != f && g < h))...
Bisher sind wir vereinfachend davon ausgegangen, dass unter einer bestimmten Bedingung immer nur eine Anweisung ausgeführt wird. Meistens müssen jedoch mehrere Anweisungen abgearbeitet werden. Um mehrere Anweisungen beim Auftreten einer bestimmten Bedingung auszuführen, müssen diese lediglich in einen Anweisungsblock zusammengefasst werden, beispielsweise:
static void Main(string[] args) {
Console.Write("Geben Sie eine Zahl zwischen 0 und 9 ein: ");
int zahl = Convert.ToInt32(Console.ReadLine());
if(zahl > 9 || zahl < 0) {
Console.WriteLine("Ihre Zahl ist unzulässig");
Console.Write("Versuchen Sie es erneut: ");
zahl = Convert.ToInt32(Console.ReadLine());
}
else {
Console.WriteLine("Korrekte Eingabe.");
Console.WriteLine("Sie beherrschen das Zahlensystem!");
}
Console.WriteLine("Die Eingabe lautet:{0}", zahl);
Console.ReadLine();
}
Listing 2.21 Mehrere Anweisungen zusammengefasst in einem Anweisungsblock
Eingebettete »if«-Statements
if-Anweisungen dürfen ineinander verschachtelt werden, d. h., dass innerhalb eines äußeren if-Statements eine oder auch mehrere weitere if-Anweisungen eingebettet werden können. Damit stehen wir aber zunächst vor einem Problem, wie im folgenden Codefragment gezeigt wird:
Console.Write("Geben Sie eine Zahl zwischen 0 und 9 ein: ");
int zahl=Convert.ToInt32(Console.ReadLine());
if(zahl >= 0 && zahl <= 9)
if(zahl <= 5)
Console.Write("Die Zahl ist 0,1,2,3,4 oder 5");
else
Console.Write("Die Zahl ist unzulässig.");
Listing 2.22 Eingebettetes »if«-Statement
Um die ganze Problematik anschaulich darzustellen, wurde auf sämtliche Tabulatoreinzüge verzichtet, denn Einzüge dienen nur der besseren Lesbarkeit des Programmcodes und haben keinen Einfluss auf die Interpretation der Ausführungsreihenfolge.
Die Frage, die aufgeworfen wird, lautet, ob else zum inneren oder zum äußeren if-Statement gehört. Wenn wir den Code betrachten, sind wir möglicherweise geneigt zu vermuten, else mit der Meldung
Die Zahl ist unzulässig.
dem äußeren if zuzuordnen, wenn eine Zahl kleiner 0 oder größer 9 eingegeben wird. Tatsächlich werden wir aber mit dieser Meldung genau dann konfrontiert, wenn eine Zahl zwischen 6 und 9 eingegeben wird, denn der Compiler interpretiert den Code wie folgt:
if(zahl >= 0 && zahl <= 9)
{
if(zahl <= 5)
Console.Write("Die Zahl ist 0,1,2,3,4 oder 5");
else
Console.Write("Die Zahl ist unzulässig.");
}
Listing 2.23 Listing 2.22 nun mit Tabulator-Einzügen
Das war natürlich nicht unsere Absicht, denn rein logisch soll die else-Klausel der äußeren Bedingungsprüfung zugeordnet werden. Um das zu erreichen, müssen wir in unserem Programmcode das innere if-Statement als Block festlegen:
if(zahl >= 0 && zahl <= 9) {
if(zahl <= 5)
Console.Write("Die Zahl ist 0,1,2,3,4 oder 5");
}
else
Console.Write("Die Zahl ist unzulässig.");
Listing 2.24 Richtige Zuordnung des »else«-Zweigs
Unsere Erkenntnis können wir auch in einer allgemeingültigen Regel formulieren:
Eine else-Klausel wird immer an das am nächsten stehende if gebunden. Dies kann nur durch das ausdrückliche Festlegen von Anweisungsblöcken umgangen werden.
Das eben geschilderte Problem der else-Zuordnung ist unter dem Begriff dangling else bekannt, zu Deutsch »baumelndes else«. Es führt zu logischen Fehlern, die nur sehr schwer aufzuspüren sind.
Es kommt in der Praxis sehr häufig vor, dass mehrere Bedingungen der Reihe nach ausgewertet werden müssen. Unter Einbeziehung der Regel über die Zuordnung der else-Klausel könnte eine differenzierte Auswertung einer eingegebenen Zahl beispielsweise wie folgt lauten:
Console.Write("Geben Sie eine Zahl zwischen 0 und 9 ein: ");
int zahl = Convert.ToInt32(Console.ReadLine());
if(zahl == 0)
Console.WriteLine("Die Zahl ist 0");
else
if(zahl == 1)
Console.WriteLine("Die Zahl ist 1");
else
if(zahl == 2)
Console.WriteLine("Die Zahl ist 2");
else
if(zahl == 3)
Console.WriteLine("Die Zahl ist 3");
else
Console.WriteLine("Zahl > 3");
Listing 2.25 Komplexeres »if«-Statement (1)
Um jedes else eindeutig zuordnen zu können, weist dieses Codefragment entsprechende Einzüge auf, die keinen Zweifel aufkommen lassen. Das täuscht dennoch nicht darüber hinweg, dass die Lesbarkeit des Codes mit wachsender Anzahl der zu testenden Bedingungen unübersichtlich wird. Unter C# bietet es sich daher an, im Anschluss an das Schlüsselwort if sofort ein else anzugeben, wie im folgenden identischen Codefragment, das wesentlich überschaubarer wirkt und damit auch besser lesbar ist:
if(zahl == 0)
Console.WriteLine("Die Zahl ist 0");
else if(zahl == 1)
Console.WriteLine("Die Zahl ist 1");
else if(zahl == 2)
Console.WriteLine("Die Zahl ist 2");
else if(zahl == 3)
Console.WriteLine("Die Zahl ist 3");
else
Console.WriteLine("Zahl > 3");
Listing 2.26 Komplexeres »if«-Statement (2)
Bedingte Zuweisung mit dem »?:«-Operator
Manchmal sehen wir uns mit der Aufgabe konfrontiert, eine Bedingung nur auf ihren booleschen Wert hin zu prüfen und in Abhängigkeit vom Testergebnis eine Zuweisung auszuführen. Eine if-Anweisung könnte dazu wie nachfolgend gezeigt aussehen:
int x, y;
Console.Write("Geben Sie eine Zahl ein: ");
x = Convert.ToInt32(Console.ReadLine());
if(x == 0)
y = 1;
else
y = x;
Gibt der Anwender die Zahl 0 ein, wird der Variablen y der Wert 1 zugewiesen. Weicht die Eingabe von 0 ab, ist der Inhalt der Variablen x mit der Variablen y identisch.
In diesem Beispiel kann auch ein von C# angebotener, spezieller Bedingungsoperator eingesetzt werden. Sehen wir uns zunächst dessen Syntax an:
<Variable> = <Bedingung> ? <Wert1> : <Wert2>
Zuerst wird die Bedingung ausgewertet. Ist deren Ergebnis true, wird Wert1 der Variablen zugewiesen, andernfalls Wert2. Damit können wir das Beispiel von oben vollkommen äquivalent auch anders implementieren:
int x, y;
Console.Write("Geben Sie eine Zahl ein: ");
x = Convert.ToInt32(Console.ReadLine());
y = x == 0 ? 1 : x;
Im ersten Moment sieht der Code schlecht lesbar aus. Wenn wir allerdings zusätzliche Klammern setzen, wird die entsprechende Codezeile schon deutlicher:
y = (x == 0 ? 1 : x);
Zuerst wird die Bedingung
x == 0
geprüft. Ist das Ergebnis true, wird y die Zahl 1 zugewiesen. Ist das Ergebnis false, werden die beiden Variablen gleichgesetzt.
2.6.2 Das »switch«-Statement
Mit der if-Anweisung können durchaus Bedingungen auf Basis sowohl verschiedener Vergleichsoperatoren als auch verschiedener Operanden formuliert werden. In der Praxis muss jedoch häufig derselbe Operand überprüft werden. Nehmen wir beispielsweise an, eine Konsolenanwendung bietet dem Anwender eine Auswahl diverser Optionen an, mit der der weitere Ablauf des Programms gesteuert werden kann:
static void Main(string[] args) {
string message = "Treffen Sie eine Wahl:\n\n";
message += "(N) - Neues Spiel\n";
message += "(A) - Altes Spiel fortsetzen\n";
message += "(E) - Beenden\n";
Console.WriteLine(message);
Console.Write("Ihre Wahl lautet: ");
string choice = Console.ReadLine().ToUpper();
if(choice == "N") {
Console.Write("Neues Spiel...");
// Anweisungen, die ein neues Spiel starten
}
else if(choice == "A") {
Console.Write("Altes Spiel laden ...");
// Anweisungen, die einen alten Spielstand laden
}
else if(choice == "E") {
Console.Write("Spiel beenden ...");
// Anweisungen, um das Spiel zu beenden
}
else {
Console.Write("Ungültige Eingabe ...");
// weitere Anweisungen
}
Console.ReadLine();
}
Listing 2.27 Komplexe Bedingungsprüfung
Der Ablauf des Programms wird über die Eingabe »N«, »A« oder »E« festgelegt. Stellvertretend wird in unserem Fall dazu eine Konsolenausgabe angezeigt. Vor der Eingabeüberprüfung sollten wir berücksichtigen, dass der Anwender möglicherweise der geforderten Großschreibweise der Buchstaben keine Beachtung schenkt. Um diesem Umstand Rechnung zu tragen, wird die Eingabe mit
string choice = Console.ReadLine().ToUpper();
in jedem Fall in einen Großbuchstaben umgewandelt. Verantwortlich dafür ist die Methode ToUpper der Klasse String, die direkt auf dem Rückgabewert aufgerufen wird.
Alternativ zur if-Struktur könnte die Programmlogik auch mit einer switch-Anweisung realisiert werden. Im obigen Beispiel müsste der if-Programmteil dann durch den folgenden ersetzt werden:
// Beispiel: ..\Kapitel 2\SwitchSample
[...]
switch(strWahl) {
case "N":
Console.Write("Neues Spiel...");
// Anweisungen, die ein neues Spiel starten
break;
case "A":
Console.Write("Altes Spiel laden...");
// Anweisungen, die einen alten Spielstand laden
break;
case "E":
Console.Write("Spiel beenden...");
// Anweisungen, um das Spiel zu beenden
break;
default:
Console.Write("Ungültige Eingabe...");
// weitere Anweisungen
break;
}
[...]
Listing 2.28 Das »switch«-Statement
Sehen wir uns nun die allgemeine Syntax der switch-Anweisung an:
// Syntax der switch-Anweisung
switch(Ausdruck) {
case Konstante1 :
// Anweisungen
Sprunganweisung;
case Konstante2 :
// Anweisungen
Sprunganweisung;
...
[default:
// Anweisungen
Sprunganweisung;]
}
Mit der switch-Anweisung lässt sich der Programmablauf ähnlich wie mit der if-Anweisung steuern. Dabei wird überprüft, ob der hinter switch aufgeführte Ausdruck, der entweder eine Ganzzahl oder eine Zeichenfolge sein muss, mit einer der hinter case angegebenen Konstanten übereinstimmt. Nacheinander wird dabei zuerst mit der Konstante1 verglichen, danach mit der Konstante2 usw. Stimmen Ausdruck und Konstante überein, werden alle folgenden Anweisungen bis zur Sprunganweisung ausgeführt. Wird zwischen dem Ausdruck und einer der Konstanten keine Übereinstimmung festgestellt, werden die Anweisungen hinter der default-Marke ausgeführt – falls eine solche angegeben ist, denn default ist optional. Achten Sie auch darauf, hinter jeder Konstanten und hinter default einen Doppelpunkt zu setzen.
Eine Sprunganweisung ist in jedem Fall erforderlich, wenn hinter dem case-Statement eine oder mehrere Anweisungen codiert sind, ansonsten meldet der Compiler einen Syntaxfehler. Die break-Anweisung signalisiert, die Programmausführung mit der Anweisung fortzusetzen, die dem switch-Anweisungsblock folgt.
Auf die Sprunganweisung kann man verzichten, wenn mehrere case-Anweisungen direkt hintereinanderstehen. Die Folge ist dann, dass die Kette so lange durchlaufen wird, bis ein break erscheint. Daher wird im folgenden Codefragment die erste Ausgabeanweisung ausgeführt, wenn value den Wert 1, 2 oder 3 hat.
int value = ...;
switch(value) {
case 1:
case 2:
case 3:
Console.Write("value = 1, 2 oder 3");
break;
case 4:
Console.Write("value = 4");
break;
}
Neben break gibt es mit goto noch eine weitere Sprunganweisung, hinter der eine Marke angegeben werden kann, beispielsweise:
goto case "E";
Die goto-Anweisung bietet sich insbesondere an, wenn für mehrere Konstanten dieselben Anweisungsfolgen ausgeführt werden müssen, z. B.:
int value = ...;
switch(value) {
case 1:
Console.WriteLine("Im case 1-Zweig");
goto case 3;
case 2:
case 3:
Console.Write("value = 1, 2 oder 3");
break;
case 4:
Console.Write("value = 4");
break;
}
Nehmen wir an, value hätte den Wert »1«. Das Programm reagiert wie folgt: Zuerst wird der case 1-Zweig ausgeführt und danach die Steuerung des Programms an den case 3-Zweig übergeben. Zwei Konsolenausgaben sind also die Folge:
Im case 1-Zweig
value = 1, 2 oder 3
Einschränkungen der »switch«-Anweisung
In C# gibt es keine Möglichkeit, einen zusammenhängenden Konstantenbereich hinter dem case-Statement anzugeben, wie es in einigen anderen Sprachen möglich ist. Wollen Sie beispielsweise für einen Ausdruck alle Zahlen im Bereich von 0 bis 10 gleichermaßen behandeln, müssen Sie für jede einzelne eine case-Anweisung implementieren. In solchen Fällen empfiehlt es sich, anstelle der switch-Anweisung das if-Statement zu verwenden.
Die »goto«-Anweisung
Die goto-Anweisung kann nicht nur innerhalb eines switch-Blocks angegeben, sondern auch generell dazu benutzt werden, eine beliebige Marke im Code anzusteuern. Solche Sprünge werden auch als unbedingte Sprünge bezeichnet, weil sie an keine besondere Bedingung geknüpft sind. Eine Marke ist ein Bezeichner, der mit einem Doppelpunkt abgeschlossen wird. Im folgenden Beispiel wird die Marke meineMarke definiert. Trifft das Programm zur Laufzeit auf das goto-Statement, verzweigt es zu den Anweisungen, die sich hinter der benutzerdefinierten Marke befinden.
static void Main(string[] args) {
int value = 4711;
Console.WriteLine("Programmstart");
goto meineMarke;
Console.WriteLine("value = {0}",value);
meineMarke:
Console.WriteLine("Programmende");
Console.ReadLine();
}
Listing 2.29 Allgemeine Verwendung von »goto«
In diesem Listing wird es niemals zu der Ausgabe des Variableninhalts von value kommen. Das ist natürlich kein Fehler, sondern mehr eine programmiertechnische Unsauberkeit, die der Compiler sogar erkennt und im Fenster Fehlerliste als Warnhinweis anzeigt.
Neben der Möglichkeit, eine Sprunganweisung innerhalb einer switch-Anweisung zu codieren, bietet sich die goto-Anweisung auch dazu an, tief verschachtelte Schleifen zu verlassen (mehr dazu im folgenden Abschnitt). In allen anderen Fällen sollten Sie jedoch prinzipiell auf goto verzichten, denn es zeugt im Allgemeinen von einem schlechten Programmierstil.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.