Kapitel 13 Anweisungen und Ausführungsverlauf
In den folgenden Abschnitten werden die in C# verfügbaren
Anweisungen näher beleuchtet.
13.1 Auswahlanweisungen
 
Die Auswahlanweisungen werden zur Durchführung
von Operationen verwendet, die auf dem Wert eines Ausdrucks basieren.
13.1.1 If
 
Die if-Anweisung in C# erfordert, dass die Bedingung innerhalb der
if-Anweisung für einen Ausdruck vom Typ bool
ausgewertet wird. Mit anderen Worten, der folgende Code ist nicht zulässig:
// Fehler
using System;
class Test
{
public static void Main()
{
int value;
if (value) // unzulässig
System.Console.WriteLine("true");
if (value == 0) // dies verwenden
System.Console.WriteLine("true");
}
}
13.1.2 Switch
 
Switch-Anweisungen sind häufig
fehleranfällig. Es passiert eben zu leicht, dass versehentlich
eine break-Anweisung am Ende eines case vergessen oder
beim Lesen des Codes ein Fehler übersehen wird.
C# beseitigt diese Fehlerquellen, indem vorgeschrieben
wird, dass am Ende jedes case-Blocks ein break
oder ein goto auf einen anderen case-Block
in der switch-Anweisung vorhanden sein muss.
using System;
class Test
{
public void Process(int i)
{
switch (i)
{
case 1:
case 2:
// Code handhabt sowohl 1 als auch 2
Console.WriteLine("Low Number");
break;
case 3:
Console.WriteLine("3");
goto case 4;
case 4:
Console.WriteLine("Middle Number");
break;
default:
Console.WriteLine("Default Number");
}
}
}
C# ermöglicht des Weiteren die Verwendung der
switch-Anweisung mit Zeichenfolgenvariablen:
using System;
class Test
{
public void Process(string htmlTag)
{
switch (htmlTag)
{
case "P":
Console.WriteLine("Paragraph start");
break;
case "DIV":
Console.WriteLine("Division");
break;
case "FORM":
Console.WriteLine("Form Tag");
break;
default:
Console.WriteLine("Unrecognized tag");
break;
}
}
}
Es ist nicht nur einfacher, statt mehrerer if-Anweisungen
eine switch-Anweisung zu schreiben, es ist zudem auch effektiver,
denn der Compiler verwendet für den Vergleich einen effizienten
Algorithmus.
Bei nur wenigen Einträgen1 in der switch-Anweisung nutzt der Compiler eine Funktion
der .NET-Laufzeitumgebung, die als Interning von Zeichenfolgen
bezeichnet wird. Die Laufzeitumgebung unterhält eine interne Tabelle
aller konstanten Zeichenfolgen, damit bei jeder Verwendung dieser Zeichenfolge
in einem einzelnen Programm dasselbe Objekt vorliegt. In der switch-Anweisung
sucht der Compiler in der Laufzeittabelle nach der switch-Zeichenfolge.
Ist diese nicht vorhanden, kann es sich bei der Zeichenfolge nicht um
eine der case-Zeichenfolgen handeln, sodass die default-Zeichenfolge
aufgerufen wird. Ist die Zeichenfolge vorhanden, wird in den internen
case-Zeichenfolgen eine sequenzielle Suche nach einer Übereinstimmung
durchgeführt.
Bei vielen Einträgen im case-Block
generiert der Compiler eine Hashfunktion sowie eine Hashtabelle und verwendet diese zur
effizienten Suche nach der Zeichenfolge2 .
13.2 Wiederholungsanweisungen
 
Wiederholungs- oder Iterationsanweisungen werden
häufig auch als Schleifenanweisungen bezeichnet und dazu eingesetzt,
Operationen durchzuführen, während eine bestimmte Bedingung
erfüllt ist.
13.2.1 While
 
Die while-Schleife funktioniert wie
erwartet: Solange die Bedingung zutrifft, wird die Schleife ausgeführt.
Wie die if-Anweisung, erfordert while eine
boolesche Bedingung:
using System;
class Test
{
public static void Main()
{
int n = 0;
while (n < 10)
{
Console.WriteLine("Number is {0}", n);
n++;
}
}
}
Die break-Anweisung kann dazu verwendet werden, die while-Schleife
zu verlassen. Die state-Anweisung kann eingesetzt werden,
um die schließende Klammer des while-Blocks für
die Iteration zu überspringen und mit dem nächsten Durchlauf
fortzufahren.
using System;
class Test
{
public static void Main()
{
int n = 0;
while (n < 10)
{
if (n == 3)
{
n++;
continue;
}
if (n == 8)
break;
Console.WriteLine("Number is {0}", n);
n++;
}
}
}
Dieser Code erzeugt die folgende Ausgabe:
0
1
2
4
5
6
7
13.2.2 Do
 
Eine do-Schleife funktioniert wie eine
while-Schleife, abgesehen davon, dass die Bedingung am
Ende der Schleife und nicht zu deren Beginn ausgewertet wird:
using System;
class Test
{
public static void Main()
{
int n = 0;
do
{
Console.WriteLine("Number is {0}", n);
n++;
} while (n < 10);
}
}
Wie bei der while-Schleife, können
die Anweisungen break und continue zur Steuerung
des Schleifenablaufs eingesetzt werden.
13.2.3 For
 
Eine for-Schleife wird zum Durchlaufen
verschiedener Werte eingesetzt. Die Schleifenvariable kann als Teil
der for-Anweisung deklariert werden:
using System;
class Test
{
public static void Main()
{
for (int n = 0; n < 10; n++)
Console.WriteLine("Number is {0}", n);
}
}
Der Bereich der Schleifenvariable in einer for-Schleife
umfasst den Bereich der Anweisung oder des Anweisungsblocks, die auf
for folgen. Der Zugriff kann nicht von außerhalb der
Schleifenstruktur erfolgen:
// Fehler
using System;
class Test
{
public static void Main()
{
for (int n = 0; n < 10; n++)
{
if (n == 8)
break;
Console.WriteLine("Number is {0}", n);
}
// Fehler; n liegt außerhalb des Bereichs
Console.WriteLine("Last Number is {0}", n);
}
}
Wie bei der while-Schleife, können
die Anweisungen break und continue zur Steuerung
des Schleifenablaufs eingesetzt werden.
13.2.4 Foreach
 
Hierbei handelt es sich um einen sehr gebräuchlichen
Schleifenausdruck:
using System;
using System.Collections;
class MyObject
{
}
class Test
{
public static void Process(ArrayList arr)
{
for (int nIndex = 0; nIndex < arr.Count; nIndex++)
{
// Typumwandlung für ArrayList erforderlich, in
dem
// Objektverweise gespeichert werden
MyObject current = (MyObject) arr[nIndex];
Console.WriteLine("Item: {0}", current);
}
}
}
Dies funktioniert zwar einwandfrei, der Programmierer
muss jedoch sicherstellen, dass das Array in der for-Anweisung
mit demjenigen Array übereinstimmt, das in der Indexoperation verwendet
wird. Wenn diese nicht übereinstimmen, kann es schwierig sein,
den Bug zu finden. Des Weiteren ist das Deklarieren einer separaten
Indexvariable erforderlich, die zufällig an anderer Stelle
verwendet werden könnte.
Darüber hinaus ist der Eingabeaufwand recht
hoch.
Einige Sprachen, beispielsweise Perl, bieten andere Konstruktionen zur Handhabung derartiger
Situationen – für C# gilt dies auch. Das vorgenannte Beispiel
kann folgendermaßen umgeschrieben werden:
using System;
using System.Collections;
class MyObject
{
}
class Test
{
public static void Process(ArrayList arr)
{
foreach (MyObject current in arr)
{
Console.WriteLine("Item: {0}", current);
}
}
}
Diese Methode ist sehr viel einfacher und birgt
weniger mögliche Fehlerquellen. Der durch die Indexoperation zurückgegebene
Typ für arr wird explizit in den Typ konvertiert,
der in foreach deklariert wurde. Das ist prima, denn Auflistungstypen
wie ArrayList können nur Werte vom Typ object
speichern.
Foreach funktioniert nicht nur für Arrayobjekte.
Tatsächlich kann foreach für jedes Objekt eingesetzt
werden, dass geeignete Schnittstellen implementiert. Beispielsweise
können mit foreach die Schlüssel einer Hashtabelle durchlaufen werden:
using System;
using System.Collections;
class Test
{
public static void Main()
{
Hashtable hash = new Hashtable();
hash.Add("Fred", "Flintstone");
hash.Add("Barney", "Rubble");
hash.Add("Mr.", "Slate");
hash.Add("Wilma", "Flintstone");
hash.Add("Betty", "Rubble");
foreach (string firstName in hash.Keys)
{
Console.WriteLine("{0} {1}", firstName, hash[firstName]);
}
}
}
Sie können benutzerdefinierte Objekte implementieren
und diese unter Verwendung von foreach durchlaufen. Weitere
Informationen hierzu finden Sie im Abschnitt »Indizierer und foreach«
in Kapitel 19, Indizierer.
Das einzige, was mit einer foreach-Schleife
nicht erreicht werden kann, ist das Ändern der Inhalte des Containers.
Wenn der Container eine Indizierung unterstützt, können die
Inhalte auf diesem Wege geändert werden, obwohl viele Container,
die die Verwendung von foreach ermöglichen, keine
Indexunterstützung bieten.
Wie bei den anderen Schleifen auch, können
für die foreach-Anweisung break und continue
verwendet werden.
13.3 Sprunganweisungen
 
Sprunganweisungen werden eben hierzu verwendet –
zum Springen von einer Anweisung zu einer anderen.
13.3.1 Break
 
Die break-Anweisung wird dazu verwendet,
den aktuellen Durchlauf oder die switch-Anweisung abzubrechen
und die Ausführung nach dieser Anweisung fortzusetzen.
13.3.2 Continue
 
Die continue-Anweisung überspringt
alle späteren Zeilen in der aktuellen Iterationsanweisung und setzt
die Ausführung der Iterationsanweisung fort.
13.3.3 Goto
 
Anhand der goto-Anweisung kann direkt
zu einem Label gesprungen werden. Da die Verwendung der goto-Anweisung
häufig als risikoreich eingestuft wird3 , verbietet C# einige der schlimmsten Missbräuche dieser Anweisung.
Eine goto-Anweisung kann beispielsweise nicht dazu verwendet
werden, in einen Anweisungsblock zu springen. Die Verwendung von goto
wird ausschließlich in switch-Anweisungen oder zur
Steuerungsübergabe aus einer verschachtelten Schleife empfohlen,
obgleich sie an beliebiger Stelle eingesetzt werden können.
13.3.4 Return
 
Die return-Anweisung kehrt zur aufrufenden
Funktion zurück und kann optional auch einen Wert zurückgeben.
13.4 Feste Zuordnung
 
Die Regeln für die feste Zuordnung verhindern
das Einsehen von Werten nicht zugewiesener Variablen. Nehmen Sie folgendes
Codebeispiel:
// Fehler
using System;
class Test
{
public static void Main()
{
int n;
Console.WriteLine("Value of n is {0}", n);
}
}
Bei der Kompilierung dieses Codeabschnitts gibt der Compiler einen Fehler aus, da der Wert
von n verwendet wird, bevor er initialisiert wurde.
Ebenso können Operationen für Klassenvariablen
nicht durchgeführt werden, bevor diese initialisiert wurden.
// Fehler
using System;
class MyClass
{
public MyClass(int value)
{
this.value = value;
}
public int Calculate()
{
return(value * 10);
}
public int value;
}
class Test
{
public static void Main()
{
MyClass mine;
Console.WriteLine("{0}", mine.value); // Fehler
Console.WriteLine("{0}", mine.Calculate()); // Fehler
mine = new MyClass(12);
Console.WriteLine("{0}", mine.value); // okay
}
}
Bei der festen Zuordnung funktionieren Strukturen
(struct-Element) etwas anders. Die Laufzeitumgebung sorgt
stets dafür, dass diese durch Nullen ersetzt werden, der Compiler
aber prüft weiterhin, ob vor einer Verwendung eine Werteinitialisierung
erfolgt.
Eine Struktur wird entweder über den Aufruf
einer Erstellungsroutine oder durch das Setzen aller Instanzenmitglieder vor der Verwendung initialisiert.
using System;
struct Complex
{
public Complex(float real, float imaginary)
{
this.real = real;
this.imaginary = imaginary;
}
public override string ToString()
{
return(String.Format("({0}, {0})", real, imaginary));
}
public float real;
public float imaginary;
}
class Test
{
public static void Main()
{
Complex myNumber1;
Complex myNumber2;
Complex myNumber3;
myNumber1 = new Complex();
Console.WriteLine("Number 1: {0}", myNumber1);
myNumber2 = new Complex(5.0F, 4.0F);
Console.WriteLine("Number 2: {0}", myNumber2);
myNumber3.real = 1.5F;
myNumber3.imaginary = 15F;
Console.WriteLine("Number 3: {0}", myNumber3);
}
}
Im ersten Abschnitt wird myNumber1
durch den Aufruf von new initialisiert. Denken Sie daran,
dass bei Strukturen keine Standarderstellungsroutine vorhanden ist,
daher geschieht bei diesem Aufruf nichts. Der einzige Nebeneffekt besteht
darin, dass die Instanz als initialisiert markiert wird.
Im zweiten Abschnitt wird myNumber2
durch einen normalen Aufruf einer Erstellungsroutine initialisiert.
Im dritten Abschnitt wird myNumber3
initialisiert, indem allen Mitgliedern der Instanz Werte zugeordnet
werden. Dies kann natürlich nur geschehen, wenn die Mitglieder
als öffentlich deklariert wurden.
13.4.1 Feste Zuordnungen und Arrays
 
Arrays weisen bei der festen Zuordnung ein etwas
anderes Verhalten auf. Bei Arrays, die aus Verweis- und Wertetypen bestehen
(Klassen und struct-Elemente) kann auf
die Arrayelemente zugegriffen werden, auch dann, wenn diese nicht initialisiert
wurden.
Angenommen, es liegt ein Complex-Array
vor:
using System;
struct Complex
{
public Complex(float real, float imaginary)
{
this.real = real;
this.imaginary = imaginary;
}
public override string ToString()
{
return(String.Format("({0}, {0})", real, imaginary));
}
public float real;
public float imaginary;
}
class Test
{
public static void Main()
{
Complex[] arr = new Complex[10];
Console.WriteLine("Element 5: {0}", arr[5]); // zulässig
}
}
Aufgrund der Operationen, die für ein Array
durchgeführt werden können – beispielsweise Reverse()
– kann der Compiler nicht in allen Situationen eine Ablaufverfolgung
der festen Zuordnungen durchführen, was zu störenden Fehlern
führen kann. Deshalb versucht er es erst gar nicht.
1
Die tatsächliche Anzahl richtet sich nach den
für jede Methode zu berücksichtigenden Leistungseinbußen.
2
Wenn Ihnen Hashfunktionen und -tabellen nicht vertraut
sind, sehen Sie sich die Klasse System.Collections.HashTable an oder
lesen Sie ein gutes Algorithmenbuch.
3
Vgl. Edsger W. Dijkstra: GO TO considered harmful:
http://www.net.org/html/history/detail/1968-goto.html.
|