Damit eine Anwendung Daten verarbeiten kann, muss Speicher für diese Daten reserviert
werden. Wie viel Speicher dafür benötigt wird, ist jedoch abhängig von den Typen der
Daten, da nicht jeder Typ gleich viel Speicher belegt. Zudem wird in C# noch zwischen
zwei Arten von Typen unterschieden, nämlich Werte- und Verweistypen.
Wertetypen sind - wie ihr Name schon sagt - Typen, die Werte direkt speichern. Das
heißt, wird von der Anwendung auf einen Wertetyp zugegriffen, dann werden die Daten
direkt aus der entsprechenden Stelle im Speicher gelesen.
Im Gegensatz dazu speichern Verweistypen nur die Adresse der Speicherstelle, an der
die eigentlichen Daten abgelegt sind. Greift die Anwendung also auf einen Verweistyp
zu, wird zunächst aus der entsprechenden Stelle im Speicher gelesen, wo sich die
eigentlichen Daten befinden, woraufhin diese in einem zweiten Schritt dann von dort
gelesen werden können.
Diese zunächst aufwändig erscheinende Trennung in Werte- und Verweistypen liegt
in der Größe der Daten begründet, die gespeichert werden sollen. Daten, deren Umfang
im Voraus bekannt ist, werden in der Regel als Wertetyp abgelegt. Da Wertetypen -
vereinfacht gesagt - in einer Tabelle im Speicher verwaltet werden, findet der Zugriff
auf diese sehr schnell statt.
Bei Daten, deren Umfang allerdings nicht von vornherein feststeht, oder deren Umfang sich
im Lauf der Zeit ändern kann, würde diese Tabelle immer wieder fragmentiert und müsste
von Zeit zu Zeit umsortiert werden. Um das zu vermeiden, werden die eigentlichen Daten
getrennt von dieser Tabelle an einer freien Adresse im Speicher abgelegt, während in
der Tabelle nur ein Verweis auf diese Adresse abgelegt wird.
Da ein Verweis auf eine Speicherstelle unabhängig von deren Adresse immer gleich viel
Speicher benötigt, kann die Tabelle problemlos genutzt werden, um diese Verweise
aufzunehmen. Dieses Verfahren löst außerdem das Problem, wie umfangreiche Daten innerhalb
einer Anwendung weitergereicht werden. Statt sämtliche Daten zu kopieren, wird lediglich
der Verweis weitergegeben, was zum einen deutlich weniger Speicher verbraucht und zum
anderen wesentlich schneller ausgeführt wird.
Allerdings ergibt sich aus der Eigenschaft, in erster Linie nur mit einem Verweis an
Stelle der eigentlichen Daten zu arbeiten, ein wesentlicher Unterschied zwischen Werte-
und Verweistypen, der beim Umgang mit diesen beachtet werden muss. Wird ein Wertetyp
kopiert, um ihn an anderer Stelle in der Anwendung zu verwenden, wird tatsächlich auf
einer Kopie gearbeitet. Veränderungen an dieser beeinflussen die ursprünglichen Daten
nicht.
Wird statt dessen aber ein Verweistyp kopiert, so wird nur der Verweis kopiert - die
eigentlichen Daten liegen nach wie vor nur ein einziges Mal im Speicher. Werden nun die
Daten der vermeintlichen Kopie verändert, ändern sich dadurch auch die ursprünglichen
Daten, denn beide Verweise zeigen auf die selbe Adresse im Speicher. Um versehentliche
Änderungen an Daten zu vermeiden, ist es wichtig, diesen Unterschied zu verinnerlichen.
Aus dieser Unterscheidung in Werte- und Verweistypen ergibt sich die Frage, welchen
Wert ein Typ enthält, wenn er zwar bereits im Speicher angelegt wurde, ihm aber noch
keine Daten zugewiesen wurden. Die Antwort auf diese Frage hängt davon ab, ob es sich
um einen Werte- oder einen Verweistyp handelt.
Während Wertetypen ein Standardwert zugewiesen wird, werden Verweistypen als
null
gekennzeichnet. Das bedeutet, dass sie derzeit nicht auf eine Speicheradresse verweisen.
Dabei ist zu beachten, dass
null ein eigener Wert ist und nicht der Zahl Null entspricht.
Außerdem muss im späteren Verlauf beim Zugriff auf einen Verweistyp stets überprüft
werden, ob überhaupt Daten vorliegen oder ob der Verweistyp
null ist.
Schließlich gibt es noch einen Hybriden zwischen Werte- und Verweistypen, nämlich die
nullbaren Wertetypen. Ihr Ursprung liegt in der Notwendigkeit, einen Wertetyp
kennzeichnen zu können, dessen Wert unbekannt oder undefiniert ist.
Häufig wird dafür der Standardwert verwendet, allerdings besteht gelegentlich Bedarf,
zwischen diesem und einem tatsächlich unbekannten oder undefinierten Wert zu unterscheiden,
wofür bei nullbaren Wertetypen dann null verwendet werden kann. Intern werden nullbare
Wertetypen allerdings als Verweistypen umgesetzt, da nur diese die Nutzung von
null
ermöglichen.
Damit bei der Entwicklung von Anwendungen nicht jeder Typ vom Benutzer entwickelt
werden muss, enthält C# eine Reihe vordefinierter Typen für einfache Daten, die
automatisch in jeder Anwendung zur Verfügung stehen.
Für Ganzzahlen bietet C# acht verschiedene Typen, die sich in erster Linie durch ihren
Wertebereich unterscheiden. Der Wertebereich berechnet sich dabei aus der Anzahl der
verfügbaren Bits, wobei nochmals zwischen vorzeichenbehafteten und vorzeichenfreien Typen
unterschieden wird. Als Standardwert verwenden diese Typen die Zahl Null.
Theoretisch sollte für eine Aufgabe zwar der am besten passende Typ verwendet werden,
in der Praxis werden allerdings fast ausschließlich
int und
long eingesetzt, da 32- und 64-Bit-Prozessoren mit diesen Typen
besser umgehen können. Außerdem wirkt sich auf Grund der Art, wie .NET Speicher für Typen
reserviert, auch der geringere Speicherbedarf der kleineren Typen - wenn überhaupt - nur
unwesentlich aus.
| sbyte |
-128 |
127 |
8 Bit |
Ja |
| short |
-32.768 |
32.767 |
16 Bit |
Ja |
| int |
-2.147.483.648 |
2.147.483.647 |
32 Bit |
Ja |
| long |
-9.223.372.036.854.775.808 |
9.223.372.036.854.775.807 |
64 Bit |
Ja |
| byte |
0 |
255 |
8 Bit |
Nein |
| ushort |
0 |
65.535 |
16 Bit |
Nein |
| uint |
0 |
4.294.967.295 |
32 Bit |
Nein |
| ulong |
0 |
18.446.744.073.709.551.615 |
64 Bit |
Nein |
Für Dezimalzahlen bietet C# drei verschiedene Typen, die sich nicht nur durch ihren
Wertebereich, sondern auch durch die Anzahl der verfügbaren Nachkommastellen
unterscheiden. Die Typen
float und
double
entsprechen dabei dem IEEE 754-Standard, der seit 1985 einen weltweit einheitlichen
Standard zur Verarbeitung von Dezimalzahlen definiert.
Der Typ
decimal hingegen verfügt zwar über einen kleineren
Wertebereich als
float und
double,
dafür aber über eine deutlich höhere Genauigkeit, was diesen Typ insbesondere für
Finanzberechnungen interessant macht.
Als Besonderheit bieten die Typen
float und
double
die Möglichkeit, die Werte +0 und -0, +∞, -∞ und NaN zu speichern. Die Werte +0 und
-0 sind vor allem beim Runden interessant. Neben +∞ und -∞ zur Darstellung positiver
und negativer Unendlichkeit können
float und
double
auch den Wert NaN - Not a Number - speichern, um ein mathematisch nicht definiertes
Ergebnis abzubilden. Für
decimal stehen diese besonderen Werte
nicht zur Verfügung.
Als Standardwert verwenden diese Typen ebenso wie die ganzzahligen Typen die Zahl Null.
| float |
±1,5 × 10-45 |
± 3,4 × 1038 |
32 Bit |
7 |
| double |
±5.0 × 10-324 |
±1.7 × 10308 |
64 Bit |
15 bis 16 |
| decimal |
±1.0 × 10-28 |
±7.9 × 1028 |
128 Bit |
28 bis 29 |
Außer diesen Typen für Ganz- und Dezimalzahlen bietet C# noch den Typ
char
zur Aufnahme eines einzelnen Zeichens, wobei Unicode voll unterstützt wird. Ein einzelnes Zeichen
wird in C# dabei durch einfache Anführungszeichen eingeschlossen. Als Standardwert wird
das Zeichen mit dem Unicode-Wert Null verwendet.
Schließlich unterstützt C# noch den Typ
bool, der zur Darstellung
der logischen Werte
true und
false dient.
Die Werte
true und
false werden - ebenso
wie
null - als Literale bezeichnet. Der Standardwert für diesen Typ
ist
false.
Obwohl ein Bit in der Theorie genügen würde, um
bool abzubilden, wird
in der Praxis ein Byte verwendet, da dies die kleinste Einheit ist, die im Speicher belegt
werden kann.
Alle bislang vorgestellten Typen sind Wertetypen, deren Speicherbedarf im Voraus
bekannt ist. Außer diesen Typen enthält C# noch zwei Verweistypen, nämlich
string und
object.
Der Typ
string dient zur Aufnahme von Text, der aus beliebig
vielen Zeichen bestehen kann. Wie
char ist auch dieser Typ
uneingeschränkt Unicode-fähig. Ein Text wird in C# durch doppelte Anführungszeichen
eingeschlossen.
Der Speicherbedarf liegt aus Leistungs- und Verwaltungsgründen bei mindestens 20 Byte,
wächst aber linear mit der Länge des zu speichernden Textes. Der Verweis an sich
belegt - je nach Speicherarchitektur - 32 oder 64 Bit.
| string |
Mindestens 20 Byte |
Um in den Typen
char und
string Sonderzeichen
wie beispielsweise einen Zeilenumbruch speichern zu können, können Zeichen nicht nur in ihrer
kanonischen Form angegeben, sondern auch als Unicode-Zeichen oder Escape-Sequenzen maskiert
werden. Ein Unicode-Zeichen wird durch einen umgekehrten Schrägstrich eingeleitet, dem ein
kleines u und die vierstellige Nummer des Zeichens folgen.
'\u0013'
Die Escape-Sequenzen beginnen ebenfalls mit einem umgekehrten Schrägstrich, bestehen
weiterhin aber nur aus einem einzelnen Zeichen, das die entsprechende Escape-Sequenz
identifiziert.
| \' |
Einfaches Anführungszeichen |
| \" |
Doppeltes Anführungszeichen |
| \\ |
Umgekehrter Schrägstrich |
| \0 |
Zeichen mit dem Unicode-Wert 0 |
| \a |
Alarmton |
| \b |
Rückschritt |
| \f |
Seitenvorschub |
| \n |
Neue Zeile |
| \r |
Wagenrücklauf |
| \t |
Horizontaler Tabulator |
| \v |
Vertikaler Tabulator |
Um die Interpretation der Escape-Sequenzen durch C# zu unterdrücken, kann einem Text
außerhalb der doppelten Anführungszeichen ein @ vorangestellt werden. Insbesondere bei
der Verwendung von Pfadangaben, die zahlreiche umgekehrte Schrägstriche enthalten,
kann dies nützlich sein - diese müssten ansonsten jeweils als Escape-Sequenz angegeben
werden.
Der Typ
object schließlich spielt eine Sonderrolle, da alle
anderen Typen von ihm abstammen. Daher kann er für jeden anderen Typ eingesetzt werden,
das heißt,
object kann einen Verweis auf beliebige Daten
speichern. Dennoch findet der Zugriff typsicher statt, so dass nach wie vor der ursprüngliche
Typ der Daten bekannt ist. Das heißt, dass beispielsweise auf einen Text nicht wie auf
eine Zahl zugegriffen werden kann, auch wenn der Text als
object
abgelegt ist.
Zudem kann jeder Typ in
object umgewandelt und von
object
wieder in den ursprünglichen Typ zurückgewandelt werden, was als Boxing beziehungsweise
Unboxing bezeichnet wird.
Neben den 32 oder 64 Bit, die für den Verweis auf die Daten anfallen, und dem Speicherplatz
für die Daten an sich, benötigt dieser Typ weitere 64 Bit für interne Verwaltungsinformationen.
Außer diesen vordefinierten Typen können Typen in C# auch vom Benutzer definiert
werden. Zu diesem Zweck gibt es einige Konzepte, auf denen benutzerdefinierte Typen
aufgebaut werden, wobei dafür wiederum verschiedene Werte- und Verweistypen zur
Auswahl stehen.
An Wertetypen bietet C# Strukturen und Enumerationen, an Verweistypen neben den im
vergangenen Kapitel erwähnten Klassen auch Schnittstellen, Arrays, Delegaten und die
im Ansatz beschriebenen nullbaren Wertetypen an. Die Definition eigener Typen auf
Basis dieser Konzepte wird in den nächsten Kapiteln im Detail beschrieben.