## 2.3 Variablen und Datentypen Dateninformationen bilden die Grundlage der Datenverarbeitung und hauchen einem Programm Leben ein: Daten können anwendungsspezifisch sein, den Zustand von Objekten beschreiben, Informationen aus Datenbanken repräsentieren oder auch nur eine Netzwerkadresse. Daten bilden also gemeinhin die Basis der Gesamtfunktionalität einer Anwendung. ### 2.3.1 Variablendeklaration Praktisch jedes Programm benötigt Daten, um bestimmte Aufgaben zu erfüllen. Daten werden in Variablen vorgehalten. Dabei steht eine Variable für eine Adresse im Hauptspeicher des Rechners. Ausgehend von dieser Adresse wird eine bestimmte Anzahl von Bytes reserviert – entsprechend dem Typ des Werts. Das, was eine Variable repräsentiert, kann vielfältiger Art sein: eine einfache Zahl, eine große Fließkommazahl, ein einzelnes Zeichen, eine Zeichenkette, eine Datums- oder Zeitangabe, aber auch die Referenz auf die Startadresse eines Objekts. Der Variablenname, auch Bezeichner genannt, dient dazu, die Speicheradresse im Programmcode mit einem Namen anzusprechen, der sich einfach merken lässt. Er ist also vom Wesen her nichts anderes als ein Synonym oder Platzhalter eines bestimmten Speicherorts. Variablen müssen deklariert werden. Unter einer Variablendeklaration wird die Bekanntgabe des Namens der Variablen sowie des von ihr repräsentierten Datentyps verstanden. Die Deklaration muss vor der ersten Wertzuweisung an die Variable erfolgen. Dabei wird zuerst der Datentyp angegeben, dahinter der Variablenname. Abgeschlossen wird die Deklaration mit einem Semikolon. Damit lautet die allgemeine Syntax:
    Datentyp Bezeichner;
Beispielsweise könnte eine zulässige Deklaration wie folgt aussehen:
    int value;
Damit wird dem Compiler mitgeteilt, dass der Bezeichner value für einen Wert steht, der vom Typ einer Ganzzahl, genauer gesagt vom Typ int (Integer) ist. Mit
    value = 1000;
wird dieser Variablen ein gültiger Wert zugewiesen. Man spricht dann auch von der Initialisierung der Variablen. Wenn Sie versuchen, auf eine nicht deklarierte Variable zuzugreifen, wird der C#-Compiler einen Fehler melden. Ebenso falsch ist es, den Inhalt einer nicht initialisierten Variablen auswerten zu wollen.
Hinweis
Variablen, die innerhalb einer Prozedur (Methode) wie beispielsweise Main deklariert sind, gelten noch nicht als initialisiert. Sie enthalten keinen gültigen Wert, auch nicht 0. Daher kann ihr Inhalt auch nicht ausgewertet werden.
Deklaration und Initialisierung können auch in einer einzigen Anweisung erfolgen:
    int value = 0;
Auf diese Weise vermeiden Sie eine nicht initialisierte Variable. Müssen Sie mehrere Variablen gleichen Typs deklarieren, können Sie die Bezeichner, getrennt durch ein Komma, hintereinander angeben:
    int a, b, c;
Sie können dann auch eine oder mehrere Variablen sofort initialisieren:
    int a, b = 9, c = 12;
### 2.3.2 Variablenbezeichner Ein Variablenname kann nahezu beliebig festgelegt werden, unterliegt aber besonderen Reglementierungen: Ein Bezeichner darf sich nur aus alphanumerischen Zeichen und dem Unterstrich zusammensetzen. Leerzeichen und andere Sonderzeichen wie beispielsweise #, §, $ usw. sind nicht zugelassen. Ein Bezeichner muss mit einem Buchstaben oder dem Unterstrich anfangen. Ein einzelner Unterstrich als Variablenname ist nicht zulässig. Der Bezeichner muss eindeutig sein. Er darf nicht gleichlautend mit einem Schlüsselwort, einer Prozedur, einer Klasse oder einem Objektnamen sein. Zur Verdeutlichung dieser Regeln folgen hier einige Beispiele für korrekte und falsche Variablenbezeichner:
    // korrekte Variablendeklarationen
    long lngMyVar;
    byte bResult_12;
    int intCarColor;
    // fehlerhafte Variablendeklarationen
    int 34M;
    string strMessage Text;
    long longSalary%Tom;
Noch ein Hinweis zur Namensvergabe: Wählen Sie grundsätzlich beschreibende Namen, damit Ihr Code später besser lesbar wird. Einfache Bezeichner wie x oder y usw. sind wenig aussagekräftig. Besser wäre eine Wahl wie farbe, gehalt, vorname usw. Nur den Zählervariablen von Schleifen werden meistens Kurznamen gegeben.
Hinweis
Die hier exemplarisch angegebenen Variablenbezeichner fangen alle mit einem Kleinbuchstaben an. Folgen Sie der allgemeinen .NET-Namenskonvention, sollten Sie bei der Wahl der Groß- und Kleinschreibung des ersten Buchstabens die Lokalität der Variablen im Code berücksichtigen. Variablen innerhalb eines Anweisungsblocks (lokale Variablen) mit einem Kleinbuchstaben zu beginnen, entspricht der Konvention. Alle anderen Fälle jetzt aufzuführen, würde momentan den Rahmen sprengen. Deshalb wird auf eine weitergehende Erläuterung an dieser Stelle verzichtet.
### 2.3.3 Zugriff auf eine Variable Wir wollen uns jetzt noch ansehen, wie wir uns den Inhalt einer Variablen an der Konsole ausgeben lassen können. Wir deklarieren dazu eine Variable vom Typ long und weisen ihr einen Wert zu, den wir danach an der Konsole ausgeben lassen.
    static void Main(string[] args)
    {
      long value = 4711;
      Console.WriteLine("value = {0}", value);
      Console.ReadLine();
    }
Deklaration und Initialisierung bieten keine Neuigkeiten, im Gegensatz zu der Anweisung, die eine Ausgabe an der Konsole bewirkt:
    Console.WriteLine("value = {0}",value);
Die Ausgabe im Befehlsfenster wird wie folgt lauten:
    value = 4711
Sie haben bereits gesehen, dass mit Console.WriteLine eine einfache Konsolenausgabe codiert wird. WriteLine ist eine Methode, die in der Klasse Console definiert ist. Jetzt fehlt noch die genaue Erklärung der verwendeten Syntax. ### 2.3.4 Ein- und Ausgabemethoden der Klasse »Console« Es bleibt uns nichts anderes übrig, als an dieser Stelle schon einen kleinen Ausflug in die Welt der Klassen und Objekte zu unternehmen. Obwohl wir uns erst ab Kapitel 3 intensiv mit diesen Themen auseinandersetzen werden, kommen wir in diesem Kapitel nicht daran vorbei, weil wir immer wieder mit den Methoden verschiedener Klassen arbeiten werden. Es handelt sich dabei meist um Methoden, um an der Eingabekonsole Ein- und Ausgabeoperationen durchzuführen: Write und WriteLine sowie Read und ReadLine. #### Die Methoden »WriteLine«, »ReadLine«, »Write« und »Read« Die Klasse Console ermöglicht es, über die beiden Methoden Write und WriteLine auf die Standardausgabeschnittstelle zuzugreifen. Der Begriff »Ausgabeschnittstelle« mag im ersten Moment ein wenig verwirren, aber tatsächlich wird darunter die Anzeige an der Konsole verstanden. WriteLine und Write unterscheiden sich dahingehend, dass die erstgenannte Methode dem Ausgabestring automatisch einen Zeilenumbruch anhängt und den Cursor in die folgende Ausgabezeile setzt. Nach dem Aufruf der Methode Write verbleibt der Eingabecursor weiterhin in der aktuellen Ausgabezeile. Ein Zeilenumbruch ist die Kombination aus Wagenrücklauf und Zeilenvorschub und entspricht den ASCII-Werten 13 und 10. Auf der Tastatur erzeugen Sie einen Zeilenumbruch durch Drücken der mit bzw. Return beschrifteten Eingabetaste. Beide Methoden sind auf vielfältige Weise einsetzbar. Denn unabhängig vom zugrunde liegenden Datentyp werden beide Ausgabemethoden ohne zu murren die gewünschten Daten im Konsolenfenster anzeigen. Wollen wir die Methode eines Objekts aufrufen, geben wir den Objektnamen an und von diesem durch einen Punkt getrennt – dies ist die sogenannte Punktnotation – den Namen der Methode. Hinter dem Methodennamen schließt sich ein Klammerpaar an. Allgemein lautet die Syntax also:
    Objektname.Methodenname();
Sie können sich mit dieser Syntax durchaus schon vertraut machen, denn sie wird Ihnen ab sofort überall begegnen, da sie in objektorientiertem Programmcode elementar ist. Das runde Klammerpaar hinter der Read- bzw. ReadLine-Methode bleibt immer leer, und bei den Methoden Write und WriteLine werden innerhalb der Klammern die auszugebenden Daten einschließlich ihres Ausgabeformats beschrieben. Allerdings dürfen auch bei den beiden letztgenannten Methoden die Klammern leer bleiben. Im einfachsten Fall kann einer der beiden Ausgabemethoden eine Zeichenfolge in Anführungsstrichen übergeben werden:
    Console.WriteLine("C# macht Spaß.");
#### Formatausdrücke in den Methoden »Write« und »WriteLine« Damit sind die Möglichkeiten der Write/WriteLine-Methoden noch lange nicht erschöpft. Die flexiblen Formatierungsmöglichkeiten erlauben die Ausgabe von Daten an beliebigen Positionen innerhalb der Ausgabezeichenfolge. Dazu dient ein Platzhalter, der auch als Formatausdruck bezeichnet wird. Dieser ist an den geschweiften Klammern zu erkennen und enthält zumindest eine Zahl. Hinter der auszugebenden Zeichenfolge werden, durch ein Komma getrennt, die Informationen übergeben, was anstelle des Formatausdrucks auszugeben ist. Sehen wir uns dazu ein Beispiel an:
    string text1 = "C#";
    string text2 = "Spaß";
    Console.Write("{0} macht {1}.", text1, text2);
Hier sind die beiden Variablen text1 und text2 vom Typ string deklariert, die mit einer in Anführungsstrichen gesetzten Zeichenfolge initialisiert werden. Die auszugebende Zeichenfolge wird in Anführungsstriche gesetzt. Getrennt durch Kommata, werden dahinter die beiden Variablen text1 und text2 bekannt gegeben. Der Inhalt der zuerst genannten Variablen text1 ersetzt den Formatausdruck {0} innerhalb der Ausgabezeichenfolge, die zweite Variable text2 ersetzt den Formatausdruck {1}. Entscheidend ist, dass dem ersten Parameter die Zahl 0 zugeordnet wird, dem zweiten die Zahl 1 usw. Die Konsolenausgabe lautet:
    C# macht Spaß.
Innerhalb des Ausgabestrings müssen die anzuzeigenden Listenelemente nicht der Reihenfolge nach durchlaufen werden. Man kann sie beliebig ansprechen oder sogar einfach ungenutzt lassen. Die Anweisung
    Console.Write("{1} macht {0}.", text1, text2);
würde demnach zu der folgenden Ausgabe führen:
    Spaß macht C#.
Der Formatausdruck {} dient nicht nur zur eindeutigen Bestimmung des Elements, er ermöglicht auch eine weitergehende Einflussnahme auf die Ausgabe. Soll der einzusetzende Wert eine bestimmte Breite einnehmen, gilt die syntaktische Variante:
    {N, M}
Dabei gilt Folgendes: N ist ein nullbasierter Zähler. M gibt die Breite der Ausgabe an. Unbesetzte Plätze werden durch eine entsprechende Anzahl von Leerzeichen aufgefüllt. Sehen wir uns dazu ein Codefragment an:
    int value = 10;
    Console.WriteLine("Ich kaufe {0,3} Eier", value);
    Console.WriteLine("Ich kaufe {0,10} Eier", value);
Die Ausgabe lautet hier:
    Ich kaufe  10 Eier
    Ich kaufe         10 Eier
Die erste Ausgabe hat eine Gesamtbreite von drei Zeichen, die Zahl selbst ist allerdings nur zwei Ziffern breit. Daher wird vor der Zahl ein Leerzeichen gesetzt. Da für die Breite der zweiten Ausgabe zehn Zeichen vorgeschrieben sind, werden links von der Zahl acht Leerstellen eingefügt. Die Breite darf auch eine negative Zahl sein. Die Ausgabe erfolgt dann allerdings linksbündig, daran schließen sich die Leerstellen an. Sie können den Formatausdruck so spezifizieren, dass numerische Ausgabedaten eine bestimmte Formatierung annehmen. Das führt uns zu der vollständigen Syntax des Formatausdrucks:
    // Syntax: Formatausdruck
    {N [,M ][: Format]}
Format spezifiziert, wie die Daten angezeigt werden. In Tabelle 2.1 werden die möglichen Optionen aufgelistet.

Tabelle 2.1 Formatangaben zur Formatausgabe

Formatangabe | Beschreibung
C | Zeigt die Zahl im lokalen Währungsformat an.
D | Zeigt die Zahl als dezimalen Integer an.
E | Zeigt die Zahl im wissenschaftlichen Format an (Exponentialschreibweise).
F | Zeigt die Zahl im Festpunktformat an.
G | Eine numerische Zahl wird entweder im Festpunkt- oder im wissenschaftlichen Format angezeigt. Zur Anzeige kommt das »kompakteste« Format.
N | Zeigt eine numerische Zahl einschließlich Kommaseparatoren an.
P | Zeigt die numerische Zahl als Prozentzahl an.
X | Die Anzeige erfolgt in Hexadezimalnotation.
An alle Formatangaben kann eine Zahl angehängt werden, aus der die Anzahl der signifikanten Stellen hervorgeht. Nachfolgend sollen einige Beispiele den Einsatz der Formatangaben demonstrieren:
    int value = 4711;
    Console.WriteLine("value={0:C}", value);
    // Ausgabe: value=4.711,00 DM
    Console.WriteLine("value={0:E}", value);
    // Ausgabe: value=4,711000E+003

    Console.WriteLine("value={0:E2}", value);
    // Ausgabe: value=4,71E+003
    int value = 225;
    Console.WriteLine("value={0:X}", value);
    // Ausgabe: value=E1
    float value = 0.2512F;
    Console.WriteLine("value={0,10:G}", value);
    // Ausgabe: value=    0,2512
    Console.WriteLine("value={0:P4}", value);
    // Ausgabe: value=25,1200%
#### Escape-Zeichen Ähnlich wie in den Sprachen C++ und Java stellt C# eine Reihe von Escape-Sequenzen zur Verfügung, die dann verwendet werden, wenn Sonderzeichen innerhalb einer Zeichenfolge ausgegeben werden sollen. Beispielsweise kann man mit dem Zeichen \n einen Zeilenumbruch erzwingen:
    Console.Write("C#\nmacht\nSpaß.");
An der Konsole wird dann
    C#
    macht
    Spaß.
angezeigt.

Tabelle 2.2 Die Escape-Zeichen

Escape-Zeichen | Beschreibung
\' | Fügt ein Hochkomma in die Zeichenfolge ein.
\'' | Fügt Anführungsstriche ein.
\\ | Fügt einen Backslash in die Zeichenfolge ein.
\a | Löst einen Alarmton aus.
\b | Führt zum Löschen des vorhergehenden Zeichens.
\f | Löst einen Formularvorschub bei Druckern aus.
\n | Löst einen Zeilenvorschub aus (entspricht der Funktionalität der -Taste).
\r | Führt zu einem Wagenrücklauf.
\t | Führt auf dem Bildschirm zu einem Tabulatorsprung.
\u | Fügt ein Unicode-Zeichen in die Zeichenfolge ein.
\v | Fügt einen vertikalen Tabulator in eine Zeichenfolge ein.
Mit Escape-Sequenzen lässt sich die Ausgabe von Sonderzeichen sicherstellen. Es ist aber auch vorstellbar, dass Zeichen, die vom Compiler als Escape-Sequenz interpretiert werden, selbst Bestandteil der Zeichenfolge sind. Fügen Sie dazu nur noch einen weiteren Schrägstrich ein. Dazu ein kleines Beispiel. Angenommen, Sie möchten die Ausgabe
    Hallo\nWelt
erzwingen. Sie müssten dann die folgende Anweisung codieren:
    Console.WriteLine("Hallo\\nWelt");
Um die Interpretation als Escape-Sequenz für eine gegebene Zeichenfolge vollständig abzuschalten, wird vor der Zeichenfolge das Zeichen @ gesetzt.
    Console.Write(@"C#\nmacht\nSpaß.");
Jetzt lautet die Konsolenausgabe:
    C#\nmacht\nSpaß.
#### Die Methoden »ReadLine« und »Read« Die Methode ReadLine liest ein oder mehrere Zeichen aus dem Eingabestrom – in unserem Fall ist das die Tastatur. Die Bereitschaft der Methode, auf Zeichen zu warten, endet mit dem Zeilenumbruch, der jedoch selbst nicht zu den eingelesenen Daten gehört. Die eingelesene Zeichenfolge wird von der Methode als Zeichenfolge vom Typ string zurückgeliefert und kann somit einer string-Variablen zugewiesen werden.
    string eingabe = Console.ReadLine();
    Console.WriteLine(eingabe);
Wir haben bisher die ReadLine-Methode dazu benutzt, um die Konsole bis zum Drücken der -Taste geöffnet zu halten. In diesem Fall war der Eingabestrom immer leer, der Rückgabewert wurde ignoriert und landete im Nirwana. Werfen wir nun einen Blick auf die Read-Methode. Diese nimmt nur ein Zeichen aus dem Eingabestrom und gibt dessen ASCII-Wert zurück. Der Rückgabewert von Read ist daher keine Zeichenfolge, sondern eine Zahl vom Typ int. Es gibt aber noch einen weiteren, nicht weniger wichtigen Unterschied zwischen Read und ReadLine: Die ReadLine-Methode liest eine ganze Zeile und benutzt den Zeilenumbruch dazu, das Ende der Eingabe zu erkennen. Danach wird der Zeilenumbruch dem Eingabestrom entnommen und gelöscht. Die Read-Methode arbeitet anders, denn der Zeilenumbruch wird nicht aus dem Eingabestrom geholt, sondern verbleibt dort und wird so lange gepuffert, bis er von einer anderen Anweisung gelöscht wird. Das kann wiederum nur die Methode ReadLine sein. Schauen Sie sich dazu das folgende Codefragment an:
    static void Main(string[] args)
    {
      int eingabe = Console.Read();
      Console.WriteLine(eingabe);
      Console.ReadLine();
    }
Nach dem Start des Programms wartet Read auf die Eingabe des Anwenders und erkennt am Zeilenumbruch das Eingabeende. Der Zeilenumbruch befindet sich weiterhin im Eingabestrom und harrt geduldig der kommenden Anweisungen. Die Anweisung in der letzten Zeile, die ReadLine-Methode, reagiert als Erstes wieder auf den Eingabestrom, erkennt darin den Zeilenumbruch und verarbeitet ihn. Das ist gleichzeitig auch das Signal, mit der nächsten Anweisung fortzufahren. Da aber das Ende der Main-Methode erreicht ist, schließt sich das Konsolenfenster sofort. Erst ein zweiter Aufruf von ReadLine würde den eigentlich angedachten Zweck erfüllen, nämlich das Fenster geöffnet zu halten und die Ausgabe der WriteLine-Methode auf unbestimmte Zeit anzuzeigen. Wenn mehrere Zeichen an der Konsole eingegeben werden, kann Read nur das erste auswerten. Alle weiteren Zeichen bleiben zusammen mit dem Zeilenumbruch im Eingabestrom. Mit
    Console.WriteLine(Console.ReadLine());
könnten Sie sich diese Zeichen anzeigen lassen. ### 2.3.5 Einfache Datentypen Die .NET-Laufzeitumgebung verfolgt das Konzept der Objektorientierung nach strengen Maßstäben. Selbst einfache Datentypen werden als Objekte angesehen, die Methoden bereitstellen, um mit einer Variablen bestimmte Aktionen auszuführen. In Tabelle 2.3 sind alle nativen Datentypen von C# zusammenfassend aufgeführt.

Tabelle 2.3 Die elementarsten Datentypen

.NET-Laufzeittyp | C#-Alias | CLS-kompatibel | Wertebereich
Byte | byte | ja | 0 ... 255
SByte | sbyte | nein | –128 ... 127
Int16 | short | ja | –215 … 215 –1
UInt16 | ushort | nein | 0 … 65.535
Int32 | int | ja | –231 … 231 –1
UInt32 | uint | nein | 0 ... 232 –1
Int64 | long | ja | –263 … 263 –1
UInt64 | ulong | nein | 0 … 264 –1
Single | float | ja | 1,4 * 10– 45 bis 3,4 * 1038
Double | double | ja | 5,0 * 10– 324 bis 1,7 * 10308
Decimal | decimal | ja | ±79E27 ohne Dezimalpunktangabe; ±7.9E–29, falls 28 Stellen hinter dem Dezimalpunkt angegeben werden. Die kleinste darstellbare Zahl beträgt ±1.0E–29.
Char | char | ja | Unicode-Zeichen zwischen 0 und 65.535
String | string | ja | ca. 231 Unicode-Zeichen
Boolean | bool | ja | true oder false
Object | object | ja | Ein Variable vom Typ Object kann jeden anderen Datentyp enthalten, ist also universell.
In der ersten Spalte ist der Typbezeichner in der .NET-Klassenbibliothek angeführt. In der zweiten Spalte steht der C#-Alias, der bei der Deklaration einer Variablen dieses Typs angegeben werden kann. Zu den Angaben in der dritten Spalte (CLS-kompatibel) muss ich Ihnen eine Erklärung geben. .NET verfolgt nicht nur ein plattformunabhängiges Konzept, sondern auch ein sprachunabhängiges. Das bedeutet, dass eine Komponente, die in einer fiktiven .NET-Programmiersprache A geschrieben wird, auch den vollen Zugriff auf alle Features einer Komponente haben sollte, die in einer anderen .NET-Sprache, nennen wir sie hier der Einfachheit halber B, implementiert ist. Das kann nur dann problemlos funktionieren, wenn sich beide auf einen gemeinsamen Nenner hinsichtlich der Sprachfeatures geeignet haben, zu denen auch die elementaren Datentypen zu rechnen sind. Dieser gemeinsame Nenner wird durch die Common Language Specification (CLS) vorgegeben. Das bedeutet: Wenn eine .NET-Anwendung CLS-konform codiert wird, ist damit eine Garantie verbunden, dass jeder andere .NET-Code Zugriff auf die (öffentlichen) Komponenten der CLS-konformen Anwendung hat – unabhängig davon, in welcher Sprache sie codiert ist. Wie Sie sehen, sind nicht alle Datentypen der Tabelle 2.3 CLS-konform. Sie können diese zwar innerhalb Ihrer Anwendung problemlos einsetzen, aber in der öffentlichen Schnittstelle haben sie nichts zu suchen.
Anmerkung
Vielleicht können Sie an dieser Stelle mit dem Begriff der öffentlichen Schnittstelle noch nicht allzu viel anfangen. Am Ende von Kapitel 4 werden Sie aber wissen, was damit gemeint ist. So lange muss ich Sie noch vertrösten.
Wie Tabelle 2.3 zu entnehmen ist, basieren alle Typen auf einer entsprechenden Definition im .NET Framework. Das hat zur Folge, dass anstelle der Angabe des C#-Alias zur Typbeschreibung auch der .NET-Laufzeittyp genannt werden kann. Damit sind die beiden folgenden Deklarationen der Variablen value absolut gleichwertig:
    int value;
    Int32 value;
#### Wertetypen und Referenztypen .NET unterscheidet zwei wesentliche Gruppen von Datentypen: Wertetypen: Zu dieser Gruppe werden die elementarsten Datentypen gezählt, z. B. Int16, Int32, Boolean, Double usw. Das entscheidende Kriterium besteht darin, dass die zugrunde liegende Typdefinition von der Klasse ValueType abgeleitet ist. Referenztypen: Zu dieser Gruppe werden alle Typen gezählt, deren Typdefinition nicht von ValueType abgeleitet ist. Das ist der überwiegende Teil aller Klassen der .NET-Bibliothek. Kennzeichnend für diese Gruppe ist, dass die Variable eines Referenztyps einen Zeiger auf einen Speicherbereich repräsentiert. In Tabelle 2.3 sind die wichtigsten und somit auch elementarsten .NET-Datentypen aufgeführt. Bis auf Object und String sind alle der Gruppe der Wertetypen zuzurechnen. Damit stellt sich auch die Frage, warum .NET diese strikte Trennung bei den Datentypen macht. Die Antwort lautet: um die Effizienz zu steigern. Da .NET konsequent den objektorientierten Ansatz verfolgt, müssen ausnahmslos alle Daten Objekte sein. Da ein Objekt jedoch auch mit einem verwaltungstechnischen Overhead verbunden ist, werden die einfachsten Datentypen erst zur Laufzeit als Objekte betrachtet. #### Initialisierung von Variablen Wenn Sie Variablen im Gültigkeitsbereich einer Methode (beispielsweise Main) deklarieren, gilt diese zwar als bekannt, jedoch nicht als mit einem bestimmten Startwert initialisiert. Sie müssen einer methodeninternen, lokalen Variablen daher ausdrücklich einen Wert zuweisen, bevor Sie zum Beispiel mit der Methode Console.WriteLine zum ersten Mal darauf lesend zugreifen. Der folgende Code führt deshalb auch zu einem Compilerfehler bei der Kompilierung:
    static void Main(string[] args)
    {
      // fehlerhafter Code, ausgelöst durch eine nicht initialisierte
      // Variable!!!
      long value;
      Console.WriteLine(value);
    }
Um eine Variable zu initialisieren, können Sie ihr bei der Deklaration einen Startwert zuweisen, z. B.:
    long value = 0;
Oder – um Ihnen schon an dieser Stelle eine andere syntaktische Variante vorzustellen – Sie verwenden den new-Operator:
    long value = new long();
Sollten Sie bei der Initialisierung oder während der Laufzeit versuchen, einer Variablen einen Wert zuzuweisen, der größer oder kleiner ist als der durch den Typ beschriebene Wertebereich, erhalten Sie einen Compiler- bzw. Laufzeitfehler. #### Ganzzahlige Datentypen C# stellt acht ganzzahlige Datentypen zur Verfügung, von denen vier vorzeichenbehaftet sind, der Rest nicht. Die uns interessierenden CLS-konformen Datentypen sind: Byte Int16 Int32 Int64 Int16, Int32 und Int64 haben einen Wertebereich, der nahezu gleichmäßig über die negative und positive Skala verteilt ist. Die vorzeichenlosen Datentypen, zu denen auch Byte gehört, decken hingegen nur den positiven Wertebereich, beginnend bei 0, ab. Der vorzeichenlose Typ Byte, der im Gegensatz zu SByte CLS-konform ist, ist insbesondere dann von Interesse, wenn auf binäre Daten zugegriffen wird. Ganzzahlige Literale können in Dezimal- oder Hexadezimalform übergeben werden. Hexadezimale Zahlen (Basis = 16) erhalten zusätzlich das Präfix 0x. Die folgende Variable hexa beschreibt die Dezimalzahl 225:
    int hexa = 0xE1;
#### Dezimalzahlen Versuchen Sie einmal, die beiden folgenden Codezeilen zu kompilieren:
    float value = 0.123456789;
    Console.WriteLine(value);
Normalerweise würde man erwarten, dass der C#-Compiler daran nichts zu beanstanden hat. Dennoch zeigt er erstaunlicherweise einen Kompilierfehler an. Wie ist das zu erklären? Auch ein Literal wie unsere Zahl 0,123456789 muss zunächst temporär in den Speicher geschrieben werden, bevor es endgültig der Variablen zugewiesen werden kann. Um eine Zahl im Speicher abzulegen, muss die Laufzeitumgebung aber eine Entscheidung treffen: Es ist die Entscheidung darüber, wie viel Speicherplatz dem Literal zugestanden wird. Das kommt aber auch der Festlegung auf einen bestimmten Datentyp gleich. Bei Dezimalzahlliteralen ist diese Festlegung immer eindeutig:
Hinweis
Literale, die eine Dezimalzahl beschreiben, werden von der .NET-Laufzeitumgebung als double-Typ angesehen.
Nun kommt es bei der Zuweisung unseres Literals an value jedoch zu einem Problem: Das Literal ist vom Typ double, und die Variable, die den Inhalt aufnehmen soll, ist vom Typ float. Per Definition weist double aber einen größeren Wertebereich als float auf – mit der Folge, dass unter Umständen vom Literal ein Wert beschrieben sein könnte, der größer ist als der, den ein float zu speichern vermag. Der Compiler verweigert deshalb diese Zuweisung. Es gibt einen sehr einfachen Ausweg aus diesem Dilemma: Man hängt dazu an das Literal ein passendes Suffix an, hier F (oder gleichwertig f), mit dem wir den Typ float für das Literal erzwingen:
    float value = 0.123456789F;
    Console.WriteLine(value);
Nun ist der C#-Compiler in der Lage, den Inhalt an der Konsole anzuzeigen – vorausgesetzt, die Zahl entspricht dem Wertebereich eines float.

Tabelle 2.4 Typsuffix der Fließkommazahlen

Suffix | Fließkommatyp
F oder f | float
D oder d | double
M oder m | decimal
#### Genauigkeit von Dezimalzahlen Die drei Typen float, double und decimal, mit denen unter C# Fließkommazahlen dargestellt werden können, beschreiben nicht nur unterschiedliche Wertebereiche, sondern auch – was im Grunde genommen noch viel wichtiger ist – unterschiedliche Genauigkeiten. Auf herkömmlichen Systemen beträgt die Genauigkeit eines float-Typs etwa zehn Stellen, die eines double-Typs etwa 16 Stellen. Abhängig ist die Genauigkeit dabei immer von der Anzahl der Ziffern des ganzzahligen Anteils der Dezimalzahl. Das folgende Codefragment demonstriert die Genauigkeit, die mit einem float erreicht werden kann:
    // --------------------------------------------------------------
    // Beispiel: ...\Kapitel 2\DezimalGenauigkeit
    // --------------------------------------------------------------
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    namespace DezimalGenauigkeit
    {
      class Program  {
        static void Main(string[] args) {
          float x, y;
          x = 0.123456789F;
          y = 0.1234567891F;
          // Prüfung, ob die Inhalte der Variablen x und y gleich sind
          if(x == y)
            Console.WriteLine("Beide Werte sind gleich.");
          else
            Console.WriteLine("Beide Werte sind ungleich.");
          Console.ReadLine();
        }
      }
    }
Es werden zunächst zwei Variablen vom Typ float deklariert. Danach wird beiden ein Wert zugewiesen, der sich nur an der zehnten Nachkommastelle unterscheidet. Mit der Anweisung
    if(x == y)
werden die Inhalte der beiden Variablen auf Gleichheit überprüft. Entspricht der von x repräsentierte Wert dem von y, soll die Meldung Beide Werte sind gleich. ausgegeben werden; weichen die Variableninhalte voneinander ab, sollte die Ausgabe Beide Werte sind ungleich. lauten. Erstaunlicherweise erscheint nach dem Start der Laufzeitumgebung die Meldung, die von den beiden Variablen beschriebenen Werte seien gleich. Diese offensichtliche Falschaussage ist darauf zurückzuführen, dass ein float nicht in der Lage ist, alle im vorliegenden Fall angegebenen Nachkommastellen exakt zu interpretieren – der Typ ist schlichtweg überfordert. Für Berechnungen, die eine höhere Genauigkeit erfordern, ist ein float daher weniger gut geeignet. Ein ähnlicher Test, diesmal mit einem double-Typ, führt zu demselben Ergebnis – allerdings tritt dieser Effekt erst auf, wenn die Anzahl der Nachkommastellen erhöht wird. Dies ist auch häufig der Grund dafür, sich für den einen oder anderen Dezimaldatentyp zu entscheiden. Die Größenordnung, die bereits ein float darstellen kann, ist per Definition schon so groß, dass hier weniger das Entscheidungskriterium zu suchen ist. Wenn von der Genauigkeit von Fließkommazahlen gesprochen wird, bedeutet das nicht, dass die Genauigkeit auf den Dezimalteil bezogen wird. Vielmehr ist es die Genauigkeit, mit der beispielsweise beim float die ersten zehn Zahlen – beginnend links mit der ersten – unterschieden werden können. Um dies zu testen, brauchen Sie nur die beiden Literale des vorhergehenden Beispiels zu ändern, z. B.:
    static void Main(string[] args) {
      float x, y;
      x = 10.123456F;
      y = 10.1234567891F;
      if(x == y)
        Console.WriteLine("Beide Werte sind gleich.");
      else
        Console.WriteLine("Beide Werte sind ungleich.");
      Console.Read();
    }
Wenn Sie diesen Code laufen lassen, werden Sie die folgende Ausgabe erhalten:
    Beide Werte sind ungleich.
Wenn Sie die Zahl 7 als letzte Nachkommastelle an die Variable x anhängen, also
    x = 10.1234567
sieht der Compiler beide Variableninhalte als gleich an. Er berücksichtigt demnach die achte Nachkommastelle nicht mehr für den Vergleich. Zum Vergleich: Im Beispiel mit einem einziffrigen ganzzahligen Anteil war es die zehnte Nachkommastelle. Die Forderung nach sehr hoher Genauigkeit einer Dezimalzahl können die beiden Datentypen float und double manchmal nicht ausreichend erfüllen. Bei noch höheren Ansprüchen muss die Wahl auf einen deutlich präziseren Datentyp fallen: decimal. Damit lassen sich Zahlen darstellen, die eine Genauigkeit von bis zu 28 Nachkommastellen aufweisen. Während die Zuweisung eines ganzzahligen Literals an eine decimal-Variable in bekannter Art und Weise erfolgt, also beispielsweise mit
    decimal decA = 120;
muss bei der Zuweisung einer numerischen Zahl mit hohem Dezimalanteil wieder ein kleiner Trick angewendet werden. Wenn Sie im Texteditor beispielsweise
    decA = 0.1234567890123456789012;
eingeben, erhalten Sie bei der Kompilierung einen Compilerfehler. Nach den Gesetzen der Typkonvertierung, die wir in Abschnitt 2.3.6, »Typkonvertierung«, behandeln werden, kann eine Zahl vom Typ int einem decimal zugewiesen werden, ohne dass es zu einem Datenverlust kommt. Der int wird dabei implizit in decimal umgewandelt. Dieselbe Aussage gilt allerdings nicht, wenn ein double einem decimal zugewiesen werden soll – es kommt zu einer Fehlermeldung in der Entwicklungsumgebung, da der Compiler ein Dezimalzahlliteral als double-Typ ansieht. Dieses Verhalten kann wieder durch ein Suffix, hier verwendet man den Buchstaben M bzw. m, vermieden werden:
    decA = 0.1234567890123456789012M;
Diese Erkenntnis wollen wir nun benutzen, um zum Abschluss die Genauigkeit zu testen, mit der eine Zahl vom Typ decimal arbeiten kann. Dazu dient uns der folgende Code:
    // --------------------------------------------------------------
    // Beispiel: ...\Kapitel 2\DecimalVergleich
    // --------------------------------------------------------------
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    namespace DecimalVergleich
    {
      class Program {
        static void Main(string[] args) {
          decimal x, y, z;
          x = 0.1234567890123456789012345678M;
          y = 0.1234567890123456789012345678M;
          z = 0.1234567890123456789012345679M;
          // Vergleich der Variablen x und y
          if(x == y)
            Console.WriteLine("x und y sind gleich.");
          else
            Console.WriteLine("x und y sind ungleich.");
          // Vergleich der Variablen x und z
          if(x == z)
            Console.WriteLine("x und z sind gleich.");
          else
            Console.WriteLine("x und z sind ungleich.");
          Console.ReadLine();
        }
      }
    }
Beachten Sie, dass die beiden Variablen x und y identisch sind, während sich x und z in der 28. Nachkommastelle unterscheiden. Wenn Sie das Programm laufen lassen, wird an der Konsole folgende Ausgabe angezeigt:
    x und y sind gleich.
    x und z sind ungleich.
Die Laufzeitumgebung erkennt den Unterschied an der 28. Nachkommastelle. Das ist natürlich eine deutliche Steigerung gegenüber float und double. Wird der ganzzahlige Anteil eines decimal vergrößert, z. B. in
    decA = 1000.1234567890123456789012346D
bewirkt jede weitere Ziffer links vom Komma einen Verlust an Genauigkeit. Im Beispiel von decA weist der Dezimalteil nur noch eine Genauigkeit von 25 Stellen rechts vom Komma auf. #### Zeichenbasierte Datentypen Variablen vom Typ char können ein Zeichen des Unicode-Zeichensatzes aufnehmen. Unicode ist die Erweiterung des ein Byte großen ASCII- bzw. ANSI-Zeichensatzes mit seinen insgesamt 256 verschiedenen Zeichen. Unicode berücksichtigt die Bedürfnisse außereuropäischer Zeichensätze, für die eine Ein-Byte-Codierung nicht ausreichend ist. Jedes Unicode-Zeichen beansprucht zwei Byte, folglich ist der Unicode-Zeichensatz auch auf 65.536 Zeichen beschränkt. Die ersten 128 Zeichen (0–127) entsprechen denen des ASCII-Zeichensatzes, die folgenden 128 Zeichen beinhalten unter anderem Sonderzeichen und Währungssymbole. Literale, die dem Typ char zugewiesen werden, werden in einfache Anführungsstriche gesetzt, zum Beispiel:
    char chrZeichen = 'A';
Um den ASCII-Wert eines einzelnen Zeichens zu erhalten, braucht man nur den Typ char einem Zahlentyp wie beispielsweise int, long oder float zuzuweisen:
    char chrZeichen = 'A';
    int intASCII = chrZeichen;
    Console.WriteLine(intASCII); // die Ausgabe lautet 65
Die implizite Umwandlung eines char in einen Zahlenwert bereitet anscheinend keine Probleme, der umgekehrte Weg – die Umwandlung eines Zahlenwerts in einen char – ist allerdings nicht möglich. char beschränkt sich nur auf ein Zeichen. Um eine Zeichenkette, die sich aus keinem oder bis zu maximal ca. 231 Einzelzeichen zusammensetzt, zu speichern oder zu bearbeiten, deklarieren Sie eine Variable vom Datentyp string. Die Einzelzeichen werden dabei wie bei char als Unicode-Zeichen der Größe 16 Bit behandelt. Zeichenketten werden grundsätzlich in doppelte Anführungsstriche gesetzt:
    string str = "C# ist spitze."
#### Weitere Datentypen Variablen vom Typ bool (Boolean) können nur zwei Zustände beschreiben, nämlich true oder false, z. B.:
    bool myBol = true;
false ist der Standardwert.
Hinweis
In vielen Programmiersprachen wird false numerisch mit 0 beschrieben und true durch alle Werte, die von 0 abweichen. .NET ist hier sehr viel strenger. Hier ist true nicht 1 und auch nicht 67, sondern ganz schlicht true. Aus diesem Grund ist auch die folgende Anweisung falsch, die so in anderen Programmiersprachen durchaus möglich ist:
    bool myBool = 2;
Das hat natürlich auch Auswirkungen auf Bedingungsprüfungen, wie Sie später noch sehen werden.
Der allgemeinste aller Datentypen ist object. Er beschreibt in seinen vier Byte einen Zeiger auf die Speicheradresse eines Objekts. Eine Variable dieses Typs kann jeden beliebigen anderen Datentyp beschreiben: Es spielt dabei keine Rolle, ob es sich um eine Zahl, eine Zeichenfolge, eine Datenbankverbindung oder um ein anderes Objekt wie zum Beispiel um die Schaltfläche in einem Windows-Fenster handelt. Zur Laufzeit wird eine auf object basierende Variable passend aufgelöst und die gewünschte Operation darauf ausgeführt. Um das zu demonstrieren, ist im folgenden Codefragment eine Variable vom Typ object deklariert, der zuerst ein Zahlenliteral und anschließend eine Zeichenfolge zugewiesen wird:
    object objUniversal;
    objUniversal = 5;
    Console.WriteLine(objUniversal);
    objUniversal = "Hallo Welt.";
    Console.WriteLine(objUniversal);
Die Variable objUniversal schluckt beide Zuweisungen anstandslos – an der Konsole wird zuerst die Zahl 5 und danach die Zeichenfolge angezeigt. Ganz gleich, was eine Variable vom Typ object beinhaltet, sie enthält grundsätzlich immer nur einen Zeiger (auch als Verweis oder Referenz bezeichnet) auf ein Objekt – selbst dann, wenn es sich um eine Zahl handelt. Herkömmliche Betriebssysteme beschreiben 32-Bit-Speicheradressen. Daraus folgt auch die einheitliche Größe dieses Datentyps von vier Byte – unabhängig davon, welcher Typ referenziert wird. #### Einfache Datentypen als Objekte Eine Variable zu deklarieren sieht harmlos und unscheinbar aus. Und dennoch, hinter dem Variablennamen verbergen sich Möglichkeiten, die Sie bisher vermutlich noch nicht erahnen. In der .NET-Laufzeitumgebung wird alles durch die objektorientierte Brille betrachtet – sogar die einfachen Datentypen. Ein simples Short soll ein Objekt sein? Wenn Sie dieser Aussage keinen Glauben schenken wollen, schreiben Sie folgende Codezeile:
    Int16.
Beachten Sie bitte hierbei den Punkt, der auf Int16 folgt. Sie werden feststellen, dass hinter der Punktangabe eine Liste aufgeklappt wird, die IntelliSense-Unterstützung (siehe Abbildung 2.4). Hier klicken, um das Bild zu vergrößern Abbildung 2.4 IntelliSense-Unterstützung in der Entwicklungsumgebung In dieser Liste sind alle Eigenschaften und Methoden aufgeführt, die ein Objekt vom Typ Int16 auszeichnen. Sie können aus dem Angebot auswählen, wenn Sie mit den Pfeiltasten zu der gewünschten Funktionalität navigieren und dann die -Taste drücken. Der ausgewählte Eintrag aus IntelliSense wird sofort vom Code übernommen, was den Vorteil hat, dass ein Schreibfehler ausgeschlossen ist. Wenn Sie beispielsweise wissen wollen, wo die wertmäßige Ober- bzw. Untergrenze des Int16-Typs liegt, könnten Sie dies mit dem folgenden Codefragment abfragen:
    Console.WriteLine("Int16(min) = {0}", Int16.MinValue);
    Console.WriteLine("Int16(max) = {0}", Int16.MaxValue);
An der Konsole erfolgt danach die Anzeige:
    Int16(min) = -32768
    Int16(max) = 32767
Wahrscheinlich werden Sie schon festgestellt haben, dass IntelliSense nicht nur im Zusammenhang mit der Punktnotation funktioniert. Sobald Sie in einer Codezeile den ersten Buchstaben eintippen, wird IntelliSense geöffnet und bietet Ihnen alle programmierbaren Optionen an, auf die mit dem eingegebenen Buchstaben zugegriffen werden kann. Die Auswahl erfolgt analog wie oben beschrieben. ### 2.3.6 Typkonvertierung Sehen wir uns die folgenden beiden Anweisungen an:
    int intValue = 12000;
    long lngValue = intValue;
Hier wird die Variable intValue vom Typ int deklariert und ihr ein Wert zugewiesen. Im zweiten Schritt erfolgt wiederum eine Variablendeklaration, diesmal vom Typ long. Der Inhalt der zuvor deklarierten Variablen intValue wird lngValue zugewiesen. Der C#-Compiler wird beide Anweisungen anstandslos kompilieren. Nun ändern wir die Reihenfolge ab, deklarieren zuerst die long-Variable, weisen ihr einen Wert von 12000 zu und versuchen dann, lngValue der int-Variablen zuzuweisen:
    long lngValue = 12000;
    int intValue = lngValue;
Diesmal ist das Ergebnis nicht wie vielleicht erwartet – der C#-Compiler quittiert die Zuweisung des long-Typs an den int-Typ mit einer Fehlermeldung, obwohl der Wertebereich eines int die Zuweisung von 12000 eindeutig verkraftet. Das auftretende Problem beruht auf einer einengenden Datentypumwandlung: Der Wertebereich eines int ist kleiner als der eines long. Im Gegensatz dazu ist die Zuweisung eines int an einen long eine aufweitende Operation, weil der long einen größeren Wertebereich als int hat. Immer dann, wenn bei einer Operation zwei unterschiedliche Datentypen im Spiel sind, muss der Typ, der rechts vom Zuweisungsoperator steht, in den Typ umgewandelt werden, der sich auf der linken Seite befindet. Man spricht hierbei auch von der Konvertierung. Prinzipiell werden zwei Arten der Konvertierung unterschieden: die implizite Konvertierung die explizite Konvertierung #### Implizite Konvertierung Eine implizite Konvertierung nimmt der C#-Compiler selbst vor. Dies setzt eine aufweitende Zuweisungsoperation voraus. Am besten schauen Sie sich dazu Abbildung 2.5 an. Die Pfeilrichtung gibt eine aufweitende, also implizite Konvertierung vor; entgegengesetzt der Pfeilrichtung wäre eine Konvertierung einengend. Demzufolge wird ein byte anstandslos implizit in einen short, int, long usw. konvertiert, aber nicht umgekehrt beispielsweise ein int in byte. Beachten Sie insbesondere, dass es keine impliziten Konvertierungen zwischen den Gleitkommatypen float/double und decimal gibt. Hier klicken, um das Bild zu vergrößern Abbildung 2.5 Implizite Konvertierung einfacher Datentypen Eine besondere Stellung nehmen bool, string, char und object ein. Mit einem bool oder einem string sind keine impliziten Konvertierungen möglich, ein char kann mit Ausnahme von byte und short jedem anderen Typ zugewiesen werden. Variablen vom Typ object wiederum unterliegen Gesichtspunkten, die wir erst ab Kapitel 3 erörtern. Unter Berücksichtigung der zuvor beschriebenen Gesetzmäßigkeiten sind die folgenden impliziten Konvertierungen möglich:
    // zulässige implizite Konvertierungen
    int intVar = 15;
    decimal decVar = intVar;
    char chrVar = 'K';
    int intValue = chrVar;
    short shtVar = 11;
    double dblVar = shtVar;
Die folgenden drei Versuche werden hingegen vom C#-Compiler abgelehnt:
    // unzulässige implizite Konvertierungen
    float fltVar = 3.12F;
    decimal decVar = fltVar;
    byte bytVar = 20;
    char c = bytVar;
    int iVar = 1;
    bool bolVar = iVar;
#### Explizite Konvertierung Unter expliziter Konvertierung versteht man die ausdrückliche Anweisung an den Compiler, den Wert eines bestimmten Datentyps in einen anderen umzuwandeln. Explizite Konvertierung folgt einer sehr einfachen Syntax: Vor dem zu konvertierenden Ausdruck wird in runden Klammern der Typ angegeben, in den die Konvertierung erfolgen soll, also:
    // Syntax: explizite Konvertierung
    (Zieldatentyp)Ausdruck
Man spricht bei den so eingesetzten runden Klammern auch vom Typkonvertierungsoperator. Mit der expliziten Konvertierung wären die folgenden beiden Zuweisungen möglich, die weiter oben noch einen Kompilierfehler verursacht haben:
    float fltVar = 3.12F;
    decimal decVar = (decimal)fltVar;
    byte bytVar = 20;
    char c = (char)bytVar;
Obwohl die explizite Konvertierung weitere Möglichkeiten eröffnet, sind ihr naturgemäß auch Grenzen gesetzt, da nur dann in einen anderen Typ umgewandelt werden kann, wenn der Zieldatentyp in Beziehung zum Ursprungsdatentyp steht (siehe Abbildung 2.5). Beispielsweise bleibt ein boolescher Wert unter .NET immer ein boolescher Wert. Damit ist die folgende Konvertierung unter C# falsch, obwohl sie in anderen Programmiersprachen durchaus zulässig ist:
    int iVar = 1;
    // fehlerbehaftete explizite Konvertierung
    bool bolVar = (bool)iVar;
Sehr ähnlich, jedoch einfacher zu verstehen ist, dass der Konvertierungsversuch eines string in einen int jeglicher Logik entbehrt:
    string strText = "Hallo";
    int intVar = strText; // FALSCH!!
#### Explizite Konvertierung mit den Methoden der Klasse »Convert« Die explizite Konvertierung mit dem Typkonvertierungsoperator ist eine Möglichkeit, einen Datentyp zu erzwingen. Eine zweite Möglichkeit bietet die .NET-Klassenbibliothek. Es handelt sich hierbei um die Klasse Convert im Namespace System, die eine Reihe von Methoden für diesen Zweck bereitstellt.

Tabelle 2.5 Die Konvertierungsmethoden der Klasse »Convert«

Methode | Beschreibung
ToBoolean (Ausdruck) | Konvertiert den Ausdruck in einen bool-Typ.
ToByte (Ausdruck) | Konvertiert den Ausdruck in einen byte-Typ.
ToChar (Ausdruck) | Konvertiert den Ausdruck in einen char-Typ.
ToDecimal (Ausdruck) | Konvertiert den Ausdruck in einen decimal-Typ.
ToDouble (Ausdruck) | Konvertiert den Ausdruck in einen double-Typ.
ToInt16 (Ausdruck) | Konvertiert den Ausdruck in einen short-Typ.
ToInt32 (Ausdruck) | Konvertiert den Ausdruck in einen int-Typ.
ToInt64 (Ausdruck) | Konvertiert den Ausdruck in einen long-Typ.
ToSByte (Ausdruck) | Konvertiert den Ausdruck in einen sbyte-Typ.
ToSingle (Ausdruck) | Konvertiert den Ausdruck in einen float-Typ.
ToString (Ausdruck) | Konvertiert den Ausdruck in einen string-Typ.
ToUInt16 (Ausdruck) | Konvertiert den Ausdruck in einen ushort-Typ.
ToUInt32 (Ausdruck) | Konvertiert den Ausdruck in einen uint-Typ.
ToUInt64 (Ausdruck) | Konvertiert den Ausdruck in einen ulong-Typ.
Damit ist das Codefragment
    long lngVar = 4711;
    int intVar = (int)lngVar;
gleichwertig mit:
    long lngVar = 4711;
    int intVar = Convert.ToInt32(lngVar);
In zwei ganz wesentlichen Punkten unterscheidet sich die Konvertierung mit den Methoden der Convert-Klasse von der mit dem Konvertierungsoperator: Grundsätzlich werden alle Konvertierungen mit den Methoden der Convert-Klasse auf einen eventuellen Überlauf hin untersucht. Es können Konvertierungen durchgeführt werden, die mit dem Typkonvertierungsoperator unzulässig sind. Den erstgenannten Punkt werden wir im folgenden Abschnitt behandeln, während wir uns an dieser Stelle zunächst nur dem zweiten Punkt zuwenden. Angenommen, wir wollen an der Eingabeaufforderung die Eingabe in einer Integervariablen speichern, muss die Anweisung dazu wie folgt lauten:
    int intDigit = Convert.ToInt32(Console.ReadLine());
Bekannterweise liefert ReadLine die Benutzereingabe als Zeichenfolge vom Typ string zurück. Wäre die Methode Convert.ToInt32 gleichwertig mit dem Typkonvertierungsoperator, würde der C#-Compiler auch die folgende Anweisung anstandslos kompilieren:
    // FALSCH!!
    int intDigit = (int)Console.ReadLine();
Allerdings wird uns der Compiler diese Anweisung mit der Fehlermeldung
    Konvertierung des Typs 'string' zu 'int' nicht möglich
quittieren, denn eine explizite Konvertierung des Typs string in einen numerischen Typ mit dem Typkonvertierungsoperator ist auch dann unzulässig, wenn die Zeichenfolge eine Zahl beschreibt. string und int stehen nach Aussage von Abbildung 2.5 in keinerlei Beziehung zueinander. Die Methoden der Klasse Convert sind aber so ausgebildet, dass in diesen Fällen dennoch eine Konvertierung erfolgt. Wie wir gesehen haben, können sich die Methoden der Convert-Klasse über diese Gesetzmäßigkeit hinwegsetzen, natürlich vorausgesetzt, dass die Konvertierung aus logischer Sicht sinnvoll ist. Solange aber eine Zeichenfolge eine Zahl beschreibt, darf auch eine Zeichenfolge durchaus in einen numerischen Typ überführt werden. #### Bereichsüberschreitung infolge expliziter Konvertierung Eine explizite Konvertierung lässt eine typeinengende Umwandlung zu. Damit drängt sich sofort eine Frage auf: Was passiert, wenn der Wert des Ausgangsausdrucks größer ist als der Maximalwert des Typs, in den konvertiert wird? Nehmen wir dazu beispielsweise an, wir hätten eine Variable vom Typ short deklariert und ihr den Wert 436 zugewiesen. Nun soll diese Variable in den Typ byte überführt werden, der den Wertebereich zwischen 0–255 beschreibt.
    short shtVar = 436;
    byte byteVar = (byte)shtVar;
    Console.WriteLine(byteVar);
Dieser Code resultiert in der folgenden Ausgabe:
    180
Um zu verstehen, wie es zu dieser zunächst unverständlichen Ausgabe kommt, müssen wir uns die bitweise Darstellung der Zahlen ansehen. Für den Inhalt der Variablen shtVar ist dies:
    436 = 0000 0001 1011 0100
Nach der Konvertierung liegt das Ergebnis 180 vor, beschrieben durch:
    180 = 1011 0100
Vergleichen wir jetzt die bitweise Darstellung der beiden Zahlen, kommen wir sehr schnell zu der Erkenntnis, dass bei einer expliziten Konvertierung mit dem Typkonvertierungsoperator beim Überschreiten der Bereichsgrenze des Zieldatentyps die überschüssigen Bits einfach ignoriert werden. Aus dem verbleibenden Rest wird schließlich die neue Zahl gebildet. Dieses Verhalten kann zu sehr schwer zu lokalisierenden, ernsthaften Fehlern in einer laufenden Anwendung führen. Wenn Sie in einer Anwendung Code entwickeln und explizit konvertieren müssen, sollten Sie daher die Kontrolle über einen eventuell eintretenden Überlauf haben. Unter C# gibt es dazu drei Alternativen: die Operatoren checked und unchecked die Einstellung im Projekteigenschaftsfenster den Verzicht auf den Typkonvertierungsoperator und stattdessen die verwendung einer Methode der Klasse Convert, die zur Auslösung einer Fehlermeldung führt #### Die Operatoren »checked« und »unchecked« Wenden wir uns zunächst den Schlüsselwörtern checked und unchecked zu, und schauen wir uns an einem Beispiel den Einsatz und die Wirkungsweise an:
    // --------------------------------------------------------------
    // Beispiel: ...\Kapitel 2\CheckedDemo
    // --------------------------------------------------------------
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    namespace CheckedDemo
    {
      class Program {
        static void Main(string[] args) {
          // Zahleneingabe anfordern
          Console.Write("Geben Sie eine Zahl im Bereich von ");
          Console.Write("0...{0} ein: ", Int16.MaxValue);
          // Eingabe einem short-Typ zuweisen
          short shtVar = Convert.ToInt16(Console.ReadLine());
          // Überlaufüberprüfung einschalten
          byte byteVar = checked((byte)shtVar);
          Console.WriteLine(byteVar);
          Console.ReadLine();
        }
      }
    }
Nach dem Starten der Anwendung wird der Benutzer dazu aufgefordert, eine Zahl im Bereich von 0 bis zum Maximalwert eines short einzugeben. Entgegengenommen wird die Eingabe durch die Methode Console.ReadLine, die ihrerseits die Eingabe als Zeichenfolge, also vom Typ string zurückliefert. Um die gewünschte Zahl einer short-Variablen zuweisen zu können, muss explizit konvertiert werden. Beachten Sie bitte, dass wir dazu die Methode ToInt16 der Klasse Convert einsetzen müssen, da eine Konvertierung eines string in einen short mit dem Typkonvertierungsoperator nicht zulässig ist:
    short shtVar = Convert.ToInt16(Console.ReadLine());
Gibt der Anwender eine Zahl ein, die den Wertebereich des short-Typs überschreitet, wird ein Laufzeitfehler ausgelöst und die Laufzeit der Anwendung beendet. Falls der Wertebereich nicht überschritten wird, wird die dann folgende Anweisung ausgeführt:
    byte byteVar = checked((byte)shtVar);
In dieser Anweisung steckt allerdings eine Gemeinheit, denn nun soll der Inhalt der short-Variablen einer byte-Variablen zugewiesen werden. Je nachdem, welche Zahl der Anwender eingegeben hat, wird die Zuweisung fehlerfrei erfolgen oder – bedingt durch die Überprüfung mit checked – zu einem Fehler führen. Löschen Sie checked aus dem Programmcode, wird die Zuweisung einer Zahl, die den Wertebereich eines byte-Typs überschreitet, in keinem Fall einen Fehler verursachen. checked ist ein Operator und wird verwendet, um einen eventuell auftretenden arithmetischen Überlauf zu steuern. Die allgemeine Syntax hierzu lautet:
    // Syntax: Der checked-Operator
    checked(Ausdruck);
Tritt zur Laufzeit ein Überlauf ein, weil der Anwender eine Zahl eingegeben hat, die den Wertebereich des Typs überschreitet, in den konvertiert werden soll, wird ein Laufzeitfehler ausgelöst, der unter .NET auch als Ausnahme bzw. Exception bezeichnet wird. Geben wir beispielsweise an der Konsole die Zahl 436 ein, werden wir die Mitteilung aus Abbildung 2.6 erhalten. Hier klicken, um das Bild zu vergrößern Abbildung 2.6 Fehlermeldung durch Überlauf Nach dem Schließen der Fehlermeldung wird die unplanmäßige Anwendung beendet. Nun könnten Sie argumentieren, dass das Beenden der Laufzeitumgebung auch nicht das sein kann, was unbedingt erstrebenswert ist. Dieses Argument ist vollkommen richtig, aber Laufzeitfehler lassen sich mittels Programmcode abfangen, und die Anwendung bleibt danach in einem ordnungsgemäßen Laufzeitzustand. Diesem Thema werden wir uns in Kapitel 7, »Weitere Möglichkeiten von C#«, noch ausgiebig widmen. Falls nicht nur ein einzelner Ausdruck, sondern mehrere Ausdrücke innerhalb eines Anweisungsblocks auf einen möglichen Überlauf hin kontrolliert werden sollen, können Sie hinter checked einen Anweisungsblock angeben, innerhalb dessen der unkontrollierte Überlauf durch die Auslösung eines Laufzeitfehlers unterbunden wird.
    checked {/*...*/}
Wie diese Variante von checked eingesetzt wird, können Sie dem nachfolgenden Beispiel entnehmen.
    static void Main(string[] args)
    {
      checked
      {
        short shtVar = 436;
        int intVar = 1236555;
        byte byteVar = (byte)shtVar;
        shtVar = (short)intVar;
        Console.WriteLine(byteVar);
        Console.ReadLine();
      }
    }
Wir können festhalten, dass wir mit checked eine gewisse Kontrolle ausüben können, falls zur Laufzeit, bedingt durch die explizite Konvertierung, ein Überlauf eintreten kann. Der Operator unchecked ist die Umkehrung der Arbeitsweise von checked: Er schaltet die Überprüfung des Überlaufs aus und ist der Standard. Während checked sich nur lokal auf den in runden Klammern stehenden Ausdruck bzw. einen eingeschlossenen Anweisungsblock bezieht, können Sie durch eine Änderung im Projekteigenschaftsfenster die Kontrolle über sämtliche auftretenden Überläufe in einer Anwendung ausüben. Öffnen Sie dieses Fenster, indem Sie im Projektmappen-Explorer das Projekt markieren, dessen Kontextmenü mit der rechten Maustaste öffnen und dann Eigenschaften wählen. Das Projekteigenschaftsfenster wird als zusätzliche Lasche im Codeeditor angezeigt. Am linken Rand werden mehrere Auswahloptionen angeboten. Um unser Problem zu lösen, müssen Sie sich für Erstellen entscheiden (siehe Abbildung 2.7). Hier klicken, um das Bild zu vergrößern Abbildung 2.7 Das Projekteigenschaftsfenster Rechts unten sehen Sie die Schaltfläche, die mit Erweitert ... beschriftet ist. Darüber wird ein Dialog geöffnet, der die gesuchte Option anbietet: Auf arithmetischen Über-/Unterlauf überprüfen (Abbildung 2.8). Markieren Sie das Kontrollkästchen, um sicherzustellen, dass eine generelle Überprüfung auf eine Über- oder Unterschreitung des Wertebereichs erfolgt. Damit vermeiden Sie Datenverlust. Hier klicken, um das Bild zu vergrößern Abbildung 2.8 Einstellen der standardmäßigen Überprüfung des Überlaufs im Projekteigenschaftsfenster