Galileo Computing < openbook > Galileo Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Über den Autor
Vorwort zur 4. Auflage
1 Allgemeine Einführung in .NET
2 Grundlagen der Sprache C#
3 Klassendesign und Vererbung
4 Weitere .NET-Datentypen
5 Weitere Möglichkeiten von C#
6 Projektmanagement und Visual Studio 2008
7 Fehlerbehandlung und Debugging
8 LINQ
9 Multithreading und asynchrone Methodenaufrufe
10 Arbeiten mit Dateien und Streams
11 Serialisierung
12 Einige wichtige .NET-Klassen
13 Grundlagen zum Erstellen einer Windows-Anwendung
14 Die wichtigsten Steuerelemente
15 Tastatur- und Mausereignisse
16 MDI-Anwendungen
17 Grafische Programmierung mit GDI+
18 Das Drucken (Printing)
19 Steuerelemente entwickeln
20 Programmiertechniken
21 WPF – die Grundlagen
22 Die Layoutcontainer
23 Die WPF-Controls
24 Konzepte von WPF
25 ADO.NET – die Verbindung zu einer Datenbank herstellen
26 Die Datenbankabfrage
27 Der SqlDataAdapter
28 Daten im lokalen Speicher – das DataSet
29 Eine Datenbank aktualisieren
30 Stark typisierte DataSets
31 Weitergabe von Anwendungen
Stichwort

Buch bestellen
Ihre Meinung?

Spacer
<< zurück
Visual C# 2008 von Andreas Kuehnel
Das umfassende Handbuch
Buch: Visual C# 2008

Visual C# 2008
geb., mit DVD
1.366 S., 49,90 Euro
Rheinwerk Computing
ISBN 978-3-8362-1172-7
Pfeil 2 Grundlagen der Sprache C#
Pfeil 2.1 Konsolenanwendungen
Pfeil 2.1.1 Ein erstes Konsolenprogramm
Pfeil 2.2 Grundlagen der C#-Syntax
Pfeil 2.2.1 Kennzeichnen, dass eine Anweisung abgeschlossen ist
Pfeil 2.2.2 Anweisungs- und Gliederungsblöcke
Pfeil 2.2.3 Kommentare
Pfeil 2.2.4 Die Groß- und Kleinschreibung
Pfeil 2.2.5 Die Struktur einer Konsolenanwendung
Pfeil 2.3 Variablen und Datentypen
Pfeil 2.3.1 Variablendeklaration
Pfeil 2.3.2 Der Variablenbezeichner
Pfeil 2.3.3 Der Zugriff auf eine Variable
Pfeil 2.3.4 Ein- und Ausgabemethoden der Klasse »Console«
Pfeil 2.3.5 Die einfachen Datentypen
Pfeil 2.3.6 Typkonvertierung
Pfeil 2.4 Operatoren
Pfeil 2.4.1 Arithmetische Operatoren
Pfeil 2.4.2 Vergleichsoperatoren
Pfeil 2.4.3 Logische Operatoren
Pfeil 2.4.4 Bitweise Operatoren
Pfeil 2.4.5 Zuweisungsoperatoren
Pfeil 2.4.6 Stringverkettung
Pfeil 2.4.7 Sonstige Operatoren
Pfeil 2.4.8 Operator-Vorrangregeln
Pfeil 2.5 Datenfelder (Arrays)
Pfeil 2.5.1 Die Deklaration und Initialisierung eines Arrays
Pfeil 2.5.2 Der Zugriff auf die Array-Elemente
Pfeil 2.5.3 Speicherabbild eines Arrays
Pfeil 2.5.4 Mehrdimensionale Arrays
Pfeil 2.5.5 Festlegen der Array-Größe zur Laufzeit
Pfeil 2.5.6 Bestimmung der Array-Obergrenze
Pfeil 2.5.7 Die Gesamtanzahl der Array-Elemente
Pfeil 2.5.8 Verzweigte Arrays
Pfeil 2.6 Kontrollstrukturen
Pfeil 2.6.1 Die »if«-Anweisung
Pfeil 2.6.2 Das »switch«-Statement
Pfeil 2.7 Programmschleifen
Pfeil 2.7.1 Die »for«-Schleife
Pfeil 2.7.2 Die »foreach«-Schleife
Pfeil 2.7.3 Die »do«- und die »while«-Schleife


Galileo Computing - Zum Seitenanfang

2.3 Variablen und Datentypen Zur nächsten ÜberschriftZur vorigen Überschrift

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.


Galileo Computing - Zum Seitenanfang

2.3.1 Variablendeklaration Zur nächsten ÜberschriftZur vorigen Überschrift

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:

// Syntax: Variablendeklaration
Datentyp Bezeichner;

Beispielsweise könnte eine zulässige Deklaration wie folgt aussehen:

int intVar;

Damit wird dem Compiler mitgeteilt, dass der Bezeichner intVar für einen Wert steht, der vom Typ einer Ganzzahl, genauer gesagt vom Typ int (Integer) ist. Mit

intVar = 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.


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 intVar = 0;

Auf diese Weise vermeiden Sie eine uninitialisierte 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;

Galileo Computing - Zum Seitenanfang

2.3.2 Der Variablenbezeichner Zur nächsten ÜberschriftZur vorigen Überschrift

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 intFarbe, dblGehalt, strVorname usw. Nur den Zählervariablen von Schleifen werden meistens Kurznamen gegeben.

Die Bezeichner von Variablen sollten mit einem Kleinbuchstaben anfangen – im Gegensatz zu den Bezeichnern von Methoden und Objekten. Wenn Sie es besonders gut machen wollen, können Sie auch ein Präfix benutzen, aus dem hervorgeht, welcher Datentyp von der Variablen beschrieben wird, z. B.:

int intVar; 
string strText;

In Tabelle 2.1 finden Sie einige typbeschreibende Präfixvorschläge.


Tabelle 2.1 Präfixvorschläge zur Variablenbezeichnung

Datentyp Präfix

short

srt

int

int

long

lng

float

flt

double

dbl

decimal

dec

char

chr

string

str

bool

bol

byte

byt



Galileo Computing - Zum Seitenanfang

2.3.3 Der Zugriff auf eine Variable Zur nächsten ÜberschriftZur vorigen Überschrift

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 lngVar = 4711; 
  Console.WriteLine("lngVar = {0}", lngVar); 
  Console.ReadLine(); 
}

Deklaration und Initialisierung bieten keine Neuigkeiten, im Gegensatz zu der Anweisung, die eine Ausgabe an der Konsole bewirkt:

Console.WriteLine("lngVar = {0}",lngVar);

Die Ausgabe im Befehlsfenster wird wie folgt lauten:

lngVar = 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.


Galileo Computing - Zum Seitenanfang

2.3.4 Ein- und Ausgabemethoden der Klasse »Console« Zur nächsten ÜberschriftZur vorigen Überschrift

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. An der Tastatur wird der Zeilenumbruch durch Drücken der mit Enter bzw. Return beschrifteten Eingabetaste erreicht.


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 – die sogenannte Punktnotation – den Namen der Methode. Hinter dem Methodennamen schließt sich ein Klammerpaar an. Allgemein lautet die Syntax also:

// die Punktnotation
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 Spass.");

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 strText1 = "C#"; 
string strText2 = "Spass"; 
Console.Write("{0} macht {1}.", strText1, strText2);

Hier sind die beiden Variablen strText1 und strText2 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 strText1 und strText2 bekannt gegeben. Der Inhalt der zuerst genannten Variablen strText1 ersetzt den Formatausdruck {0} innerhalb der Ausgabezeichenfolge, die zweite Variable strText2 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 Spass.

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}.", strText1, strText2);

würde demnach zu der folgenden Ausgabe führen:

Spass macht C#.

Der Formatausdruck {} dient nicht nur der 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 intVar = 10; 
Console.WriteLine("Ich kaufe {0,3} Eier", intVar); 
Console.WriteLine("Ich kaufe {0,10} Eier", intVar);

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 nummerische 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.2 werden die möglichen Optionen aufgelistet.


Tabelle 2.2 Formatangaben der 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 intVar = 4711; 
Console.WriteLine("intVar={0:C}", intVar); 
// Ausgabe: intVar=4.711,00 DM

Console.WriteLine("intVar={0:E}", intVar); 
// Ausgabe: intVar=4,711000E+003

Console.WriteLine("intVar={0:E2}", intVar); 
// Ausgabe: intVar=4,71E+003

int i = 225; 
Console.WriteLine("i={0:X}", i); 
// Ausgabe: i=E1

float fltVar = 0.2512F; 
Console.WriteLine("fltVar={0,10:G}", fltVar); 
// Ausgabe: fltVar=    0,2512

Console.WriteLine("fltVar={0:P4}", fltVar); 
// Ausgabe: fltVar=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\nSpass.");

An der Konsole wird dann

C# 
macht 
Spass.

angezeigt.


Tabelle 2.3 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 Enter -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\nSpass.");

Jetzt lautet die Konsolenausgabe:

C#\nmacht\nSpass.

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 strEingabe = Console.ReadLine(); 
Console.WriteLine(strEingabe);

Wir haben bisher die ReadLine-Methode dazu benutzt, um die Konsole bis zum Drücken der Enter -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.


Galileo Computing - Zum Seitenanfang

2.3.5 Die einfachen Datentypen Zur nächsten ÜberschriftZur vorigen Überschrift

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.4 sind alle nativen Datentypen von C# zusammenfassend aufgeführt.


Tabelle 2.4 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 … 65535

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 65535

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 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.4 CLS-konform. Sie können diese zwar innerhalb Ihrer Anwendung problemlos einsetzen, aber in der öffentlichen Schnittstelle haben sie nichts zu suchen.


Abbildung 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.4 zu entnehmen ist, basieren alle Typen auf einer Klassendefinition 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 intVar absolut gleichwertig:

int intVar; 
Int32 intVar;

Wertetypen und Referenztypen

.NET unterscheidet zwei wesentliche Gruppen von Datentypen:

1. 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.
2. 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.4 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 lngVar; 
  Console.WriteLine(lngVar); 
}

Um eine Variable zu initialisieren, können Sie ihr bei der Deklaration einen Startwert zuweisen, z. B.:

long lngVar = 0;

oder – um Ihnen schon an dieser Stelle eine andere syntaktische Variante vorzustellen – Sie verwenden den new-Operator:

long lngVar = 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 beschriebenen 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 intHex beschreibt die Dezimalzahl 225:

int intHex = 0xE1;

Dezimalzahlen

Versuchen Sie einmal, die beiden folgenden Codezeilen zu kompilieren:

float fltValue = 0.123456789; 
Console.WriteLine(fltValue);

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:


Literale, die eine Dezimalzahl beschreiben, werden von der .NET-Laufzeitumgebung als double-Typ angesehen.


Nun kommt es bei der Zuweisung unseres Literals an fltValue 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 fltValue = 0.123456789F; 
Console.WriteLine(fltValue);

Nun ist der C#-Compiler in der Lage, den Inhalt an der Konsole anzuzeigen – vorausgesetzt, die Zahl entspricht dem Wertebereich eines float.


Tabelle 2.5 Typsuffix der Fließkommazahlen

Suffix Fließkommatyp

F oder f

float

D oder d

double

M oder m

decimal


Die 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 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 wir bei der Kompilierung einen Compilerfehler. Nach den Gesetzen der Typkonvertierung, die wir in Abschnitt 2.3.6 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 zu arbeiten in der Lage ist. 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 Progarm { 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. Lassen Sie das Programm laufen, 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.1234567890123456789012346M

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 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, z. B.:

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.

Abbildung 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: 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, spielt dabei keine Rolle. 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.

Die einfachen 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 simpler 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).

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 Tab -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.


Galileo Computing - Zum Seitenanfang

2.3.6 Typkonvertierung topZur vorigen Überschrift

Sehen wir uns die folgenden beiden Anweisungen an:

int intVar = 12000; 
long lngVar = intVar;

Hier wird die Variable intVar 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 intVar wird lngVar zugewiesen. Der C#-Compiler wird beide Anweisungen anstandslos kompilieren.

Nun ändern wir die Reihenfolge ab, deklarieren zuerst die long-Variable, weisen ihr den Wert von 12000 zu und versuchen dann, lngVar der int-Variablen zuzuweisen:

long lngVar = 12000; 
int intVar = lngVar;

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

Die implizite Konvertierung

Eine implizite Konvertierung nimmt der C#-Compiler selbst vor. Dies setzt eine aufweitende Zuweisungsoperation voraus. Am besten schauen wir uns dazu Abbildung 2.5 an.

Abbildung 2.5 Die implizite Konvertierung einfacher Datentypen

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.

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;

Die 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.6 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:

1. Grundsätzlich werden alle Konvertierungen mit den Methoden der Convert-Klasse auf einen eventuellen Überlauf hin untersucht.
2. 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:

int intDigit = (int)Console.ReadLine(); // FALSCH!!

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 folgende Mitteilung erhalten (siehe Abbildung 2.6).

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 dieses Buches noch ausgiebig widmen.

Abbildung 2.6 Fehlermeldung durch Überlauf

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 nachfolgendem 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, kann durch eine Änderung im Projekteigenschaftsfenster die Kontrolle über sämtliche auftretenden Überläufe in einer Anwendung ausgeübt werden. Ö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).

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.

Abbildung 2.8 Einstellen der standardmäßigen Überprüfung des Überlaufs im Projekteigenschaftsfenster

Jetzt kann man auf alle expliziten Angaben von checked verzichten, denn die Überprüfung des Unter- bzw. Überlaufs wird in der Anwendung zum Standard erklärt. Möchte man aus bestimmten Gründen auf die Überprüfung verzichten, kommt der Operator unchecked ins Spiel und hebt für den entsprechenden Ausdruck die Überprüfung wieder auf.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen. >> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Visual C# 2008
Visual C# 2008
Jetzt bestellen


 Ihre Meinung?
Wie hat Ihnen das <openbook> gefallen?
Ihre Meinung

 Buchtipps
Zum Katalog: Visual C# 2012






 Visual C# 2012


Zum Katalog: Professionell entwickeln mit Visual C# 2012






 Professionell
 entwickeln mit
 Visual C# 2012


Zum Katalog: Windows Presentation Foundation






 Windows Presentation
 Foundation


Zum Katalog: Schrödinger programmiert C++






 Schrödinger
 programmiert C++


Zum Katalog: C++ Handbuch






 C++ Handbuch


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo




Copyright © Rheinwerk Verlag GmbH 2008
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Rheinwerk Computing]

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de