abstract
, bool
, catch
oder class
. Die meisten werden im Folgenden besprochen.
Sie erkennen die Schlüsselwörter meist daran, dass sie in einer IDE wie dem Visual Studio farbig hervorgehoben werden (in diesem Buch wegen fehlender Farben dagegen fett).
Die Schlüsselwörter werden in C# immer klein geschrieben.
abstract | as | base | bool | break | byte |
case | catch | char | checked | class | const |
continue | decimal | default | delegate | do | double |
else | enum | event | explicit | extern | false |
finally | fixed | float | for | foreach | goto |
if | implicit | in | int | interface | internal |
is | lock | long | namespace | new | null |
object | operator | out | override | params | private |
protected | public | readonly | ref | return | sbyte |
sealed | short | sizeof | stackalloc | static | string |
struct | switch | this | throw | true | try |
typeof | uint | ulong | unchecked | unsafe | ushort |
using | virtual | volatile | void | while |
Tabelle 4.1: Übersicht der Schlüsselwörter von C#
Daneben existieren so genannte Kontextschlüsselwörter. Sie dienen nicht dazu, Anweisungen im Programmcode zu formulieren, sondern versehen bestimmte Stellen mit einer
Art Markierung. Mit partial
können Sie beispielsweise mehrere Klassendeklarationen markieren, die dann später zusammengeführt werden.
add | get | global | join | let | orderby |
partial | remove | select | set | value | var |
where | yield |
Tabelle 4.2: Übersicht der Kontextschlüsselwörter von C#
int ergebnis; string personName;Die Bezeichner sind in diesem Fall
ergebnis
und personName
. An die Namensgebung von Bezeichnern werden einige besondere Anforderungen gestellt:
Name
und NAME
sind also zwei verschiedene Bezeichner.@class
.
int PascalSchreibweise; int kamelSchreibweise; int 0FalscheSchreibweise; // weil er mit einer Zahl beginnt int äöü; // erlaubt, aber nicht sinnvoll
INFO
Verwenden Sie für lokale und geschützte Variablen- sowie Parameternamen die Kamelschreibweise, für Typen (Klassen, Interfaces, Aufzählungen), Methodennamen, Ereignisnamen, Konstanten, Namespaces und Eigenschaften die Pascalschreibweise. An den entsprechenden Stellen wird noch einmal auf die Schreibweise hingewiesen.
TIPP
Wählen Sie für Bezeichner immer aussagekräftige, passende Namen wie userName oder benutzerName und keine Abkürzungen wie un (in diesem Buch werden zum Teil kurze Variablennamen genutzt, um Zeilenumbrüche zu vermeiden und den Quellcode einigermaßen übersichtlich darzustellen). Orientieren Sie sich durchgängig an der englischen Sprache für die Namensvergabe, wenn Sie den Code international bearbeiten oder vertreiben.Der erste Kommentar gibt lediglich wieder, was die folgende Anweisung für eine Bedeutung hat, und sollte deshalb weggelassen werden. Der zweite Kommentar ist schon etwas aussagekräftiger, da er einen Hinweis liefert, warum die folgende Anweisung verwendet wird.
// hier wird Text ausgegeben Console.WriteLine("Hallo"); // Die diskrete Fourier-Transformation bietet die beste Performance Fourier.Transform();
TIPP
Wenn Sie Kommentare verwenden, um komplizierten Code zu erläutern, sollten Sie in erster Linie darüber nachdenken, den Code zu vereinfachen. Komplizierter Code kann schnell zu Fehlern führen, insbesondere wenn man ihn selbst nicht so richtig verstanden hat.
C# definiert zwei Kommentartypen. Ein dritter Typ wird zur automatisierten Generierung von Dokumentationen genutzt (vgl. Kapitel 26).
Letzterer ist aber nicht Bestandteil der Sprache C#. Mittels des C#-Compilers und der Option /doc
kann die Dokumentation in eine XML-Datei geschrieben
und weiterverarbeitet werden.
Einzeilige Kommentare beginnen mit der Zeichenfolge //
und laufen bis zum Ende der aktuellen Zeile. Mehrzeilige
Kommentare werden in die Zeichenfolgen /*
und */
eingeschlossen. Sie dürfen nicht verschachtelt werden. Dokumentationskommentare beginnen
mit der Zeichenfolge ///
und gelten bis zum Ende einer Zeile. Allerdings sind für eine sinnvolle Anwendung noch weitere Angaben notwendig.
// einzeiliger Kommantar int index; // speichert den aktuellen Index in einem Array /* Mehrzeilige Kommentare beschreiben meist eine umfangreichere Eigenschaft einer Anwendung. */ // Jetzt beginnt gleich ein Dokumentationskommentar /// <summary> /// Dokumentationskommentare haben eine spezielle Syntax /// </summary>
public void LeereFunktion { // TODO Hier muss noch etwas getan werden }
Wie man im Beispiel sieht, können Anweisungsblöcke an beliebiger Stelle beginnen und mehrere Anweisungen zusammenfassen.
Console.WriteLine("Hallo"); // Dies ist eine Anweisung // Jetzt beginnt ein Anweisungsblock { Console.WriteLine("Erste Anweisung im Anweisungsblock"); Console.WriteLine("Zweite Anweisung im Anweisungsblock"); }
Im folgenden Beispiel wird im Namespace CSharpBuch.Kap04
zweimal die Klasse CName
deklariert. Der Bezeichner CName
ist
damit nicht eindeutig und Sie können das Programm nicht übersetzen. Anders sieht es mit der Variablen zahl
aus. Beim ersten Auftreten von zahl
handelt es sich um eine Instanzvariable, da die Variable in jeder Instanz der Klasse (bei jedem Exemplar) vorhanden ist. Außerdem kann von jeder Methode
der Klasse auf diese Variable zugegriffen werden. Beim zweiten Auftreten von zahl
in der Methode Test()
handelt es sich um eine lokale
Variable, d.h. die Variable zahl
ist nur lokal innerhalb der Methode gültig. Nach dem Verlassen der Methode "gibt es die lokale Variable zahl
nicht
mehr", d.h., der Zugriff auf diese Variable ist dann nicht mehr möglich.
namespace CSharpBuch.Kap04 { public class CName { private int zahl; public void Test() { int zahl; // verdeckt die "äußere" Variable zahl zahl = 10; } } public class CName // <==== Jetzt nicht eindeutig { } }
Damit die Variablen in der statischen Methode Main()
verwendet werden können, müssen sie ebenfalls als statisch deklariert werden.
Statische Variablen sind nicht an ein Objekt gebunden, sondern an eine Klasse, d.h., sie existieren genau einmal innerhalb der Klasse. Innerhalb
der Methode Main()
kann auf "äußere" Variablen wie zahl1
zugegriffen werden. Sie können aber auch innerhalb einer Methode
Variablen deklarieren (so genannte lokale Variablen).
Das Beispiel wird nicht fehlerfrei übersetzt, wenn versucht wird, die uninitialisierte lokale Variable zahl2
auf der rechten Seite in der
Berechnung zu verwenden, d.h. innerhalb einer Zuweisung (siehe Kommentar). Bei einer Wertzuweisung wird der Wert des rechten Ausdrucks der
linken Variablen zugewiesen.
public class Variablen { static int zahl0 = 10; static int zahl1; public static void Main() { int zahl2; // erzeugt einen Fehler, da zahl2 nicht initialisiert ist // zahl1 = zahl2 + 10; zahl2 = 10; zahl1 = zahl2 + 10; } }
Sie können in einer Anweisung auch mehrere Variablen deklarieren.
int zahl1, zahl2 = 10;
Variablen, die in einer Klasse, aber außerhalb einer Methode deklariert werden, werden auch als Field (Feld) bezeichnet.
var
(var
steht nicht für Variant, sondern für Variable) leitet die Variablendeklaration ein. Es folgen der
Bezeichner und eine Wertzuweisung. Der Compiler ermittelt nun den Typ des Wertes und verwendet diesen Typ für die Variable. Implizit typisierte Variablen
können in normalen Deklarationen, for
- und einer foreach
-Schleifen sowie der using
-Anweisung verwendet werden.
Implizit typisierte Variablen werden außerdem noch bei anonymen Typen eingesetzt (siehe Kapitel 5). Im Gegensatz zu normalen
Variablen können implizit typisierte Variablen nur einzeln deklariert werden.
var zahl = 10; var zahl = 10, zahl2 = 11; // Fehler !
Es werden vier verschiedene Varianten gezeigt, wie eine Variable implizit mit einem Typ versehen werden kann, einmal als normale Variable,
als Array (vgl. Kapitel 10) und innerhalb einer for
- und foreach
-Anweisung. Im ersten Fall wird
auch einmal der durch den Compiler generierte Typ - hier int
- ausgegeben.
using System; namespace CSharpBuch.Kap04 { public class ImplizitTypisiert { static void Main(string[] args) { var zahl = 10; Console.WriteLine(zahl.GetType().ToString()); var zahlenArray = new[] { 10, 11, 12 }; foreach(int zahl2 in zahlenArray) Console.WriteLine(zahl2); for(var i = 1; i < 10; i++) Console.WriteLine(i); } } }Listing 4.2: ImplizitTypisiert.cs
static
darf aber nicht explizit angegeben werden. Die Deklaration einer Konstanten beginnt mit dem Schlüsselwort
const
, gefolgt vom Datentyp, dem Namen und dem Wert der Konstanten.
Es wird eine Konstante erstellt, welche die aktuelle Mehrwertsteuer in Prozent speichert.
using System; public class Konstanten { const int MWST = 19; public static void Main() { Console.WriteLine("Die Mehrwertsteuer beträgt aktuell: " + MWST + " Prozent"); } }Listing 4.3: Konstanten.cs
Der Einsatz von Konstanten hat in bestimmten Situationen Vorteile gegenüber der Angabe eines konkreten Wertes (Literals):
SEKUNDEN_PER_STUNDE
sicher besser verständlich als der Wert 3600
.
int
und double
zum Speichern von ganzen und Gleitkommazahlen.
Für jeden einzelnen Werttyp wird ein eigener Speicherbereich bereitgestellt. Eine Variable von einem Werttyp steht also direkt für den verwalteten Wert.
Referenztypen verweisen dagegen auf einen bestimmten Speicherbereich, d.h., sie speichern lediglich einen Verweis. Dadurch ist es möglich, dass mehrere (Referenz-)Variablen
auf den gleichen Speicherbereich verweisen. Insbesondere bedeutet dies, dass bei einer Zuweisung zwischen Werttypen immer eine Kopie des Wertes erstellt wird, während
bei Referenztypen lediglich eine weitere Referenz auf einen Speicherbereich hinzugefügt wird (und evt. eine Referenz auf einen anderen Speicherbereich dadurch wegfällt).
In Abbildung 4.1 wird dies noch einmal ersichtlich. Jede der Variablen i
und j
besitzt einen eigenen Speicherbereich, der direkt den Wert der Variablen enthält.
Abbildung 4.1: Werttypen speichern direkt den Wert
In der Abbildung 4.2 wird eine Zuweisung mit Referenztypen durchgeführt. Jede der Variablen al1
und al2
besitzt einen eigenen Speicherbereich,
der eine Referenz enthält. Beide Variablen verweisen aber auf den gleichen Speicherbereich, in dem sich das eigentliche Zielobjekt, eine ArrayList (eine Liste, die mehrere
Objekte verwaltet), befindet. In Wirklichkeit erstellen Sie also keine Kopie der ArrayList, sondern nur einen weiteren Verweis darauf.
Abbildung 4.2: Referenztypen speichern nur Referenzen auf Daten
Zur Verwaltung der Daten einer Anwendung werden zwei getrennte Speicherbereiche verwendet, der Stack und der Heap. Werttypen und lokale Variablen werden auf dem Stack verwaltet. Wird eine Methode aufgerufen, werden die Parameter und die lokalen Variablen auf dem Stack abgelegt und nach dem Verlassen wieder von dort entfernt. Weiterhin werden auf dem Stack die Referenzvariablen selbst gespeichert. Der über die Referenzvariablen referenzierte Datenbereich befindet sich auf dem Heap. Der Speicher auf dem Heap wird durch den Garbage Collector verwaltet.
true
und
false
annehmen. Literale sind Konstanten, die keinen Bezeichner besitzen, z.B. die Zahl 101
, die Zeichenkette "CSharpBuch"
oder auch die booleschen
Werte true
und false
. Im Namespace System
befinden sich die originalen Typdefinitionen der Werttypen. So ist der Typ int
z.B. die
Kurzschreibweise für den Typ System.Int32
. Das heißt, dass unter .NET, anders als bei vielen anderen Programmiersprachen, sämtliche Datentypen Objekte sind und damit
auch Methoden besitzen. Das entsprechende Schlüsselwort (wie z.B. int
) steht also nur als Kurzschreibweise für einen tatsächlichen Typ. In der folgenden Tabelle werden zuerst
die Kurzschreibweisen und darunter die "echten" Typnamen angegeben. Alle Typen befinden sich im Namespace System
.
Typ | Beschreibung |
byte und sbyte Byte und SByte | Vorzeichenloser bzw. vorzeichenbehafteter 8-Bit Typ Wertebereich: 0...255 bzw. -128...127 |
ushort und short UInt16 und Int16 | Vorzeichenloser bzw. vorzeichenbehafteter 16-Bit Typ Wertebereich: 0...65535 bzw. -32768...32767 |
uint und int UInt32 und Int32 | Vorzeichenloser bzw. vorzeichenbehafteter 32-Bit Typ Wertebereich: -2147483648...2147483647 bzw. 0...4294967295 |
ulong und long UInt64 und Int64 | Vorzeichenloser bzw. vorzeichenbehafteter 64-Bit Typ Wertebereich: 0...1020 bzw. -1019...1019 |
float Single | Gleitkommatyp mit einfacher Genauigkeit (7 Stellen) Wertebereich: -1038...1038 |
double Double | Gleitkommatyp mit doppelter Genauigkeit (15 Stellen) Wertebereich: -10308...10308 |
bool Boolean | Boolescher Datentyp mit den Werten true und false |
char Char | 16-Bit-Unicode-Zeichentyp, wird in Apostrophe eingeschlossen, z.B. 'a' |
decimal Decimal | Genauer Gleitkommatyp mit 28 signifikanten Stellen Wertebereich: -1028...1028 |
Weitere Werttypen sind Strukturen (struct
) und Aufzählungen (enum
), deren konkrete Ausprägungen durch den Entwickler definiert werden. Beide werden später noch vorgestellt.
Alle Werttypen (die intern als Struktur implementiert sind) sind (indirekt) von der Klasse System.ValueType
abgeleitet und besitzen deshalb auch Methoden.
So lässt sich von einem Werttyp in Kurzschreibweise der konkrete Typ über die Methode GetType()
bestimmen. Das Ergebnis des Methodenaufrufs ist vom Typ Type
.
Mittels dessen Eigenschaft Fullname
können Sie den Namen des Typs ausgeben.
using System; namespace CSharpBuch.Kap04 { public class WerteTypen { static void Main() { int zahl = 10; char c = 'A'; double abweichung = 0.123; Console.WriteLine(zahl.GetType().FullName); Console.WriteLine(c.GetType().FullName); Console.WriteLine(abweichung.GetType().FullName); Console.ReadLine(); } } }Listing 4.4: WerteTypen.cs
Als Ergebnis erhalten Sie die folgende Ausgabe:
Abbildung 4.3: Ausgabe der vollständigen Typnamen
Werttypen lassen sich auch wie Referenztypen erstellen. In diesem Fall werden sie sogar mit einem Standardwert initialisiert. Allerdings ist diese Schreibweise ziemlich aufwändig.
Int32 i32 = new Int32(); int i = i32;
string
und object
. Während Strings Zeichenketten verwalten, speichern Objekte komplexere Datenkonstruktionen.
Wie bereits erläutert, sind Referenztypen nur Verweise auf die entsprechenden Daten welche auf dem Heap gespeichert werden.
Typ | Beschreibung |
object Object | Basistyp aller anderen Typen |
string String | Repräsentiert eine Zeichenkette. Der Wert eines String-Typs ist immutable, d.h. unveränderbar. Wird einem String ein anderer Wert zugewiesen, wird ein neues String-Objekt erzeugt. |
Wenn Sie die nächsten Kapitel durcharbeiten, werden Ihnen weitere Referenztypen begegnen, wie z.B. Klassen, Interfaces, Delegaten und Arrays (Felder).
String
verwaltet. Ein Zeichen wird darin immer im Unicode-Zeichensatz
gespeichert, d.h., pro Zeichen werden 2 Byte Platz benötigt und Sie können so ziemlich alle Zeichen damit kodieren, die Sie irgendwann einmal benötigen. Zeichenketten vom Typ String
werden immer in Anführungszeichen gesetzt, z.B. "Text"
. Eine leere Zeichenkette wird durch zwei aufeinander folgende Anführungszeichen gekennzeichnet ""
.
In der Literatur und in Beispielen werden beide Schreibweisen string
(als Schlüsselwort) und String
(der Typname) gleichermaßen verwendet.
Welche Sie verwenden bleibt Ihnen überlassen.
In einer Zeichenkette lassen sich außerdem einige "Sonderzeichen", so genannte Escapesequenzen, verwenden. Diese dienen z.B. zur Darstellung des Anführungszeichens " selbst oder zum Einfügen eines Zeilenumbruchs. Eine vollständige Liste der Escapezeichen finden Sie in der Hilfe - suchen Sie nach Escapezeichen.
Escapesequenz | Beschreibung |
\" und \\ | Fügen die Zeichen " und \ in einen String ein. |
\n | Erzeugt einen Zeilenumbruch. |
\t | Fügt einen Tabulator ein. |
\uxxxx | Entspricht einem Unicode-Zeichen, die x werden durch Hexadezimalzahlen ersetzt. Die Zeichenkette "Test" kann z.B. auch so geschrieben werden:
"\u0054e\u0073t" , wobei \u0054 für den Buchstaben T und \u0073 für s steht. |
string dateiName = "C:\\Temp\\Daten.txt"; string dateiName = @"C:\Temp\Daten.txt";
byte
-Typ implizit in einen int
-Typ konvertiert werden, da der Wertebereich des int
-Typs
den Wertebereich des byte
-Typs umfasst. Die umgekehrte Konvertierung ist nur explizit möglich (d.h., Sie müssen den Zieltyp explizit in Klammern davor schreiben), da
nicht sichergestellt werden kann, dass der int
-Typ einen Wert zwischen 0 und 255 enthält. Explizite Konvertierungen werden auch als Cast oder TypeCast bezeichnet.
byte b = 10; int i = 1000; i = b; // implizite Konvertierung (automatisch) b = (byte)i; // explizite Konvertierung (manuell)
Durch eine explizite Datentypkonvertierung kann es zu Datenverlusten kommen, wenn die Wertebereiche über- bzw. unterschritten werden. Die folgende Anweisung wird vom
Compiler zurückgewiesen, da er bereits überprüft, ob diese Zuweisung einen Überlauf erzeugt. Einem byte
-Typ können eigentlich nur Werte zwischen 0 und 255 zugewiesen werden.
b = (byte) 256;
Die folgenden Anweisungen tricksen den Compiler aus, der jetzt nicht überprüfen kann, welchen Wert die Variable i
zur Ausführung der Konvertierung hat
(theoretisch könnte er es in diesem Fall wissen). Statt einer Fehlermeldung besitzt die Variable b
nun den Wert 0. Nachdem der letzte mögliche Wert, nämlich 255,
überschritten wurde, wird sozusagen am anderen Ende wieder begonnen, also bei 0.
int i = 256; b = (byte)i;
checked
eingeschlossenen, meldet der Compiler bei Überläufen einen Fehler bzw. es wird zur Laufzeit eine OverflowException
ausgelöst.
Das Ergebnis der Prüfung hängt maßgeblich davon ab, ob es sich um einen konstanten Ausdruck oder einen erst zur Laufzeit berechneten Ausdruck handelt. In einem unchecked-Kontext
wird das höchstwertige Bit abgeschnitten, so dass der neue Wert verfälscht wird.
Wenn Sie diese Anwendung ausführen (dazu vorher die Zeile b = (byte) 256;
auskommentieren), wird zuerst ein fehlerhafter Wert von 255 für die Variable b
angezeigt und danach eine Exception ausgelöst.
using System; public class Variablen { public static void Main() { byte b = 0; unchecked { b = (byte)511; Console.WriteLine(b); } checked { b = (byte)256; // Compilerfehler b = (byte)(b * 1000); // OverflowException Console.WriteLine(b); } } }Listing 4.5: Ueberlaufpruefung.cs
Durch die Verwendung der Compileroption /checked mit einem folgendem + oder - können Sie das Verhalten für alle nicht markierten Ausdrücke explizit aktivieren bzw. deaktivieren, z.B.
> csc /checked- Test.csUm die Überlaufprüfung im Visual Studio zu (de)aktivieren öffnen Sie die Projektoptionen (Menüpunkt , Register ERSTELLEN, Button ERWEITERT, Option AUF ARITHMETISCHEN ÜBER-/UNTERLAUF ÜBERPRÜFEN.
object
und System.ValueType
konvertiert
werden. Der neue, geboxte Wert ist eine Kopie des originalen Wertes, so dass sich Änderungen nicht auf das Original auswirken. Dies ist ein Unterschied zur allgemeinen Verwendung
von Referenztypen, die eigentlich immer einen Verweis auf die konkrete Instanz besitzen und somit jederzeit eine Manipulation erlauben.
Beim Unboxing kann von den Typen object
und System.ValueType
in einen beliebigen Werttyp konvertiert werden. Beachten Sie aber, dass die Konvertierung auch
möglich sein muss, da ansonsten eine Exception ausgelöst wird.
In der dritten Zeile des Beispiels wird das Boxing des int
-Wertes nach object durchgeführt. Der int
-Wert wird dazu in ein Int32
-Objekt verpackt.
Der umgekehrte Weg benötigt die Angabe eines Casts (int
), um aus dem object
- wieder einen int
-Typ zu erzeugen. Wenn Sie versuchen, den geboxten int
-Typ
beispielsweise in einen byte
-Typ zu konvertieren, wird eine InvalidCastException ausgelöst.
int i = 10; object IntObj; IntObj = i; i = (int)IntObj; i = (byte)IntObj;
Wenn Sie noch keine Erfahrungen mit Methoden und Objekten haben, lesen Sie diesen Abschnitt am besten nach dem Durcharbeiten des folgenden Kapitels erneut. Das Thema Boxing gehört zwar zum Thema Datentypkonvertierungen, benötigt aber bereits Basiswissen der objektorientierten Programmierung.
Ein mögliches Anwendungsszenario für die Verwendung von (Un)Boxing sind Datenstrukturen, die Elemente unterschiedlichen Typs (z.B. int
- und double
-Werte)
verwalten. Die Methoden, die ein Element in eine solche Struktur einfügen bzw. daraus zurückliefern, verwenden in diesem Fall als Parameter- und Rückgabetyp den Typ object
.
Die Methode Addition()
erwartet zwei Parameter vom Typ object
, wandelt diese durch Unboxing in den entsprechenden Werttyp int
um und liefert deren
Summe zurück. In der Methode Addition()
wird über die Methode GetType()
der Typ der Parameter ausgegeben. Auf diese Weise können Sie vor dem Unboxing überprüfen,
ob der korrekte Typ vorliegt. Das Boxing erfolgt bei der Parameterübergabe an die Methode Addition()
automatisch.
using System; namespace CSharpBuch.Kap04 { public class Boxing { static void Main(string[] args) { int ergebnis = (int)Addition(10, 11); Console.WriteLine("Das Ergebnis ist {0}", ergebnis); Console.ReadLine(); } static object Addition(object zahl1, object zahl2) { int z1 = (int)zahl1; int z2 = (int)zahl2; Console.WriteLine("Die Parameter sind vom Typ {0}", zahl1.GetType()); return z1 + z2; } } }Listing 4.6: Boxing.cs
2+3*4
zuerst die Multiplikation durchgeführt. Um die Reihenfolge der Auswertung festzulegen, verwenden Sie
Klammern. Dies führt außerdem zu einer besseren Lesbarkeit und vermeidet Missverständnisse bei Unkenntnis der Vorrangregeln. So wird im Ausdruck ((2+3)*4)
zuerst die
Addition durchgeführt. Bei gleicher Wertigkeit der Operatoren wird ein Ausdruck immer von links beginnend ausgewertet.
Operatoren | Beschreibung |
+ - * / % | Plus, Minus, Multiplikation, Division (mit Rest im Falle von ganzen Zahlen), Modulo (Rest einer ganzzahligen Division) |
++ -- | Inkrement, Dekrement - jeweils als Postfix- und Präfixnotation |
! & | ^ ~ | NOT, AND, OR, XOR, bitweises Komplement |
&& || | Bedingtes AND, OR |
?: | Bedingungsoperator |
= *= /= %= += -= &= ^= |= | Zuweisungen in Verbindung mit einer Operation werden immer von rechts nach links durchgeführt. |
== != > < >= <= | Gleichheit, Ungleichheit, größer als, kleiner als, größer oder gleich, kleiner oder gleich |
[] () | Feldzugriff, Klammerung für Casts und Ausdrücke |
. | Memberzugriff |
:: | Namespace-Aliasqualifizierer |
<< >> <<= >>= | bitweises Links- und Rechtsschieben sowie in Verbindung mit Zuweisung |
-> | Zeigerdereferenzierung |
?? | Auswertung von nullbaren Typen |
int zahl1 = 10 / 3; // => zahl1 = 3 int zahl2 = 11 % 2; // => zahl2 = 1 int zahl3 = zahl1++; // => zahl3 = 3, zahl1 = 4 zahl3 = ++zahl1; // => zahl3 = 5, zahl1 = 5 int zahl4 = zahl2 > zahl3 ? 1 : 0; // => zahl4 = 0 zahl1 += zahl2; // => zahl1 = 6 bool ergebnis = zahl1 == zahl2; // => ergebnis = false zahl1 = zahl2 = zahl3; // => zahl1 = 5, zahl2 = 5
Einfachste Ausdrücke bestehen aus Literalen oder einfachen Berechnungen.
10 "Ich bin ein Ausdruck" ((12 + 13) * 14)
==
und nicht versehentlich
das Zuweisungszeichen =
verwendet wird.
bool wert1 = false, wert2 = true; wert1 = ((wert1 && wert2) || wert1); wert1 = (wert1 == wert2); if(wert1 = wert2) // => Compilerfehler
null
besitzen. Ein Sonderfall sind Strings, die genau dann gleich sind, wenn sie die gleiche Zeichenkette beinhalten oder null
sind.
Das Schlüsselwort null
ist ein Literal und kennzeichnet einen Verweistyp, der auf nichts zeigt. Er ist nicht initialisiert.
if
-Anweisung ausgedrückt werden. In Klammern wird hinter if
der
Ausdruck formuliert. Liefert er das Ergebnis true
zurück, werden die Anweisungen im if
-Zweig ausgeführt.
int zahl1 = 10, zahl2 = 0; if(zahl2 > 0) zahl1 = zahl1 / zahl2;Was macht man aber im anderen Fall? Oft wird eine Alternative benötigt, wenn aufgrund eines Bedingungsausdrucks Anweisungen nicht ausgeführt werden können. Dies wird durch einen
else
-Zweig erreicht. Dieser wird genau dann ausgeführt, wenn die Bedingung der if
-Anweisung nicht zutrifft.
int zahl1 = 10, zahl2 = 0; if(zahl2 > 0) zahl1 = zahl1 / zahl2; else zahl1 = 0;Wird wie im verwendeten Beispiel jeweils nur eine Anweisung in den beiden Zweigen ausgeführt, ist diese immer mit einem Semikolon abzuschließen. Mehrere Anweisungen können in Anweisungsblöcke eingeschlossen werden.
Mit einer if
-else
-Anweisung kann z.B. die Division durch 0 verhindert werden. In diesem Fall wird der Variablen zahl2
der
Wert 0 zugewiesen und eine Textausgabe durchgeführt.
using System; namespace CSharpBuch.Kap04 { public class IfElseAnweisung { static void Main(string[] args) { int zahl1 = 10, zahl2 = 0; if(zahl2 > 0) zahl1 = zahl1 / zahl2; else { zahl1 = 0; Console.WriteLine("Durch 0 kann nicht geteilt werden!"); } } } }Listing 4.7: IfElseAnweisung.cs
else
-Zweig von weiteren Bedingungen abhängt. Wenn Sie mehrere if
-else
-Zweige
verwenden, gehört der else
-Zweig immer zum letzten if
-Zweig, der noch keinen else
-Zweig besitzt. Durch eine Klammerung und
Einrückungen können Sie die Zugehörigkeiten besser hervorheben.
if(...) ... else if() ... else ...
?:
kann verkürzt für die folgende Schreibweise einer if
-Anweisung genutzt werden. Zuerst wird ein Ausdruck angegeben, der zu true
oder false
ausgewertet wird. Wird er zu true
ausgewertet, wird der Wert nach dem Fragezeichen der Variablen zugewiesen, im anderen Fall der Wert nach dem Doppelpunkt.
int zahl1 = 10, zahl2; if(zahl1 > 10) zahl2 = 1; else zahl2 = 2; // oder über den Bedingungsoperator zahl2 = (zahl1 > 10) ? 1 : 2;
if
-else
-Anweisung
ungeeignet. Es würden zu viele Verschachtelungen entstehen. Einen Ausweg bietet die switch
-Anweisung. In Klammern wird nach der Anweisung switch
ein Ausdruck (der
so genannte Selektor) angegeben, der zu einem ganzzahligen oder einen Zeichentyp (char
oder string
unter Beachtung der Groß- und Kleinschreibung)
ausgewertet wird. In mehreren case
-Zweigen wird der Wert des Ausdrucks mit einem konstanten Wert vom Typ des Selektors verglichen. Liefert dieser Vergleich true
,
werden die Anweisungen des case
-Zweigs ausgeführt.
Die switch
-Anweisung besitzt die folgenden Eigenschaften:
switch
-Anweisung wird ein Ausdruck angegeben, der einen ganzzahligen oder einen Zeichentyp zurückgibt. Dessen Wert wird dann in den folgenden case
-Zweigen geprüft.
case
-Zweig angegeben werden.
case
-Zweigs dem Selektor, werden die folgenden Anweisungen bis zu einer abschließenden Sprunganweisung ausgeführt. Diese
kann break
, return
oder ein goto
sein. Die Angabe einer Sprunganweisung für jeden case
-Zweig ist Pflicht.
case
-Zweige. Wird hier keine Sprunganweisung angegeben, werden die Anweisungen des nächsten case
-Zweigs
ausgeführt, der über Anweisungen verfügt. Auf diese Weise können Sie Bereiche bilden.
switch(selektor) { case wert1: case wert2: case wert3: ... // Anweisungen break; }
case
-Zweige dürfen nicht den gleichen Wert abprüfen.
default
-Zweig angegeben werden. Entspricht der Wert des Selektors keinem der Werte der case
-Zweige, wird dieser Zweig im
Sinne einer Standardverarbeitung ausgeführt. Der default
-Zweig muss nicht zwingend am Ende stehen, benötigt aber wie auch die case
-Zweige ein abschließendes break
.
goto
-Anweisung zu einen anderen Zweig aufrufen, mit dem die Abarbeitung fortgesetzt werden soll, z.B. goto case "xxx";
oder goto default;
.
Über diese Anwendung werden Sie aufgefordert, ein Akronym einzugeben, das aus dem .NET-Sprachgebrauch stammt. Die Eingabe wird über eine switch
-Anweisung ausgewertet und die Anwendung danach beendet.
using System; namespace CSharpBuch.Kap04 { public class SwitchAnweisung { static void Main(string[] args) { string eingabe = ""; Console.WriteLine("Geben Sie ein .NET-Acronym ein:"); eingabe = Console.ReadLine(); switch(eingabe) { case "CLR": Console.WriteLine("Common Language Runtime"); break; case "CTS": Console.WriteLine("Common Type System"); break; case "CLS": Console.WriteLine("Common Language Spec."); break; default: Console.WriteLine("Keine bekannte Abkürzung"); break; } } } }Listing 4.8: SwitchAnweisung.cs
for
-Anweisung erhalten Sie zu diesem Zweck die Möglichkeit, eine Initialisierung, eine
Aktualisierung und einen Ausdruck zu formulieren.
for(Initialisierung; Ausdruck; Aktualisierung)Bei Erreichen der ersten Auswertung der
for
-Anweisung wird die Initialisierung durchgeführt. Danach erfolgt die Auswertung des Ausdrucks.
Der Inhalt der for
-Anweisung wird so lange ausgeführt, wie der Ausdruck zu true
ausgewertet werden kann. Nach jedem vollständigen
Schleifendurchlauf wird die Aktualisierung durchgeführt.
Die Zählvariable i
wird während der Initialisierung mit dem Wert 1 vorbelegt. Die Anweisungen der for
-Schleife werden so lange
ausgeführt, wie der Wert 11 durch i
nicht erreicht wird. Bei jedem Schleifendurchlauf wird der Wert von i
um 1 erhöht. Die Schleife wird also zehnmal durchlaufen.
int i; for(i = 1; i < 11; i++) { // tue etwas }
for
-Anweisung hat weiterhin die folgenden Eigenschaften:for
-Schleife beschränkt. Mehrere Initialisierungen werden durch Kommata voneinander getrennt.
char
sein.
true
oder false
liefert. Teilausdrücke können mit den entsprechenden Operatoren verknüpft werden.
Die Grenzen für das Abbruchkriterium können auch durch Variablen festgelegt werden. So könnte sich z.B. der Wert der Variablen Ende
bei jedem Aufruf der for
-Schleife
ändern. Das Abbruchkriterium besteht aus Ausdrücken, die über den Operator & miteinander verknüpft werden. Die Initialisierung wurde etwas "komplexer" formuliert und
ist nicht nur auf die Inkrementierung eines Wertes wie i++
beschränkt.
int ende = 11; for(int i = 0, j = 0; (i < 11) & (j < ende); i += 2, j = i + 1) Console.WriteLine(i * j);
In diesem Beispiel werden die Produkte der Zahlen 1 bis 10 auf der Konsole ausgegeben.
using System; namespace CSharpBuch.Kap04 { public class ForAnweisung { static void Main(string[] args) { for(int i = 1; i <= 10; i++) Console.WriteLine(i + " * " + i + " = " + i * i); } } }Listing 4.9: ForAnweisung.cs
int j = 10; for(int i = 1; i <= j; i++) { j--; Console.WriteLine(i + " * " + i + " = " + i * i); }
hase
) vom Typ der Elemente des Arrays definiert. Dann werden über die Anweisung in hasenListe
die Elemente des Arrays durchlaufen.
In der Methode Main()
wird zunächst ein Array (Arrays werden später noch genauer besprochen) aus drei Strings (Hasennamen
) erstellt.
Dann wird das Array über eine for
- und eine foreach
-Schleife durchlaufen. Der Vorteil der foreach
-Schleife liegt vor allem darin,
dass Sie sich nicht um die Laufvariable und die Länge des Arrays kümmern müssen.
using System; namespace CSharpBuch.Kap04 { public class ForEachAnweisung { static void Main(string[] args) { string[] hasenListe = {"Nelly", "Daisy", "Mandy"}; for(int i = 0; i < hasenListe.Length; i++) Console.WriteLine(hasenListe[i]); foreach(string hase in hasenListe) Console.WriteLine(hase); } } }Listing 4.10: ForEachAnweisung.cs
foreach
-Anweisung stehen zwei Einschränkungen gegenüber. Erstens können Sie der Iterationsvariablen (z.B. hase
)
keinen neuen Wert zuweisen und auf diese Weise die Werte der einzelnen Elemente ändern. Zweitens verfügen Sie nicht über einen aktuellen Index, z.B. um die Fundstelle eines
Elements zu bestimmen. Benötigen Sie diese beiden Funktionalitäten, müssen Sie auf die "normale" for
-Anweisung zurückgreifen oder mit Hilfsvariablen arbeiten.
foreach
-Anweisung wird insbesondere dann interessant, wenn Sie damit den Inhalt eigener Typen durchlaufen. So kann eine Klasse z.B. eine
Liste von Bauteilen verwalten. Durch eine Erweiterung der Klasse können Sie diese Liste dann ebenfalls über foreach
ausgeben. Mehr Informationen dazu gibt es im Kapitel über Auflistungen.for
-Anweisung steht die Anzahl der Schleifendurchläufe von Beginn an fest, bzw. die Anweisungen sollen nur eine bestimmte Anzahl oft
ausgeführt werden. Außerdem wird eine Zählvariable (auch Laufvariable genannt) zur Verfügung gestellt. Mittels der while
-Anweisung wird nur das Abbruchkriterium definiert.
Eine Laufvariable gibt es nicht. So kann z.B. über eine while
-Anweisung beliebig oft auf eine Benutzereingabe reagiert werden, bis das Abbruchkriterium
erreicht wurde, zum Beispiel die Eingabe von "Ende"
. Dazu ist keine Laufvariable notwendig. Der Unterschied der while
- zur do
-while
-Anweisung
liegt darin, dass im ersteren Fall das Abbruchkriterium zu Beginn, im zweiten Fall am Ende der Schleife überprüft wird. Eine do
-while
-Anweisung wird
also mindestens einmal ausgeführt.
while(...Ausdruck ...) { // Anweisungen } do { // Anweisungen } while(...Ausdruck ...);Die
while
-Anweisungen haben die folgenden Eigenschaften:
true
liefert.
do
-while
) überprüft.
Die folgende Anwendung bestimmt die Anzahl der Zeichen einer vom Benutzer eingegebenen Zeichenkette. Dies wird so lange durchgeführt (aber mindestens einmal - deshalb die do
-while
-
Anweisung), bis der Benutzer die Zeichenkette Ende
eingibt.
using System; namespace CSharpBuch.Kap04 { public class WhileAnweisung { static void Main(string[] args) { string eingabe = ""; Console.WriteLine("Anzahl der Zeichen bestimmen..."); Console.WriteLine("Die Eingabe >Ende< beendet die Anwendung"); do { Console.WriteLine("Bitte geben Sie einen Text ein: "); eingabe = Console.ReadLine(); Console.WriteLine("Zeichenzahl: " + eingabe.Length); } while(eingabe != "Ende"); } } }Listing 4.11: WhileAnweisung.cs
break
und continue
haben Sie außerdem
die Möglichkeit, eine Schleife an einer beliebigen Stelle sofort zu verlassen bzw. direkt zur erneuten Prüfung des Abbruchkriteriums zu verzweigen. Sind mehrere Schleifen ineinander verschachtelt,
wird über break
immer nur die (innere) Schleife verlassen, in der break
aufgerufen wird. Die beiden Anweisungen haben die folgenden Eigenschaften:
break
können Sie for
-, while
-, do
-while
- und switch
-Blöcke verlassen.
continue
können Sie in for
-, while
- und do
-while
-Blöcken direkt zur Auswertung des Abbruchkriteriums verzweigen.
Es werden alle Zahlen zwischen 1 und 100 ausgegeben, die ohne Rest durch 2 teilbar sind. Mittels break
wird geprüft, ob bereits der obere Grenzwert von 100
überschritten wurde. In diesem Fall wird die Anwendung beendet. Über die continue
-Anweisung wird zum nächsten Schleifendurchlauf verzweigt, wenn die aktuelle Zahl nicht
ohne Rest durch 2 teilbar ist.
using System; namespace CSharpBuch.Kap04 { public class BreakContinueAnweisung { static void Main(string[] args) { int zahl = 0; do { if(zahl > 100) break; zahl++; if((zahl % 2) != 0) continue; Console.WriteLine(zahl); } while(true); } } }Listing 4.12: BreakContinueAnweisung.cs
goto
ist verpönt und führt bei Missbrauch - mehrfach ineinander verschachtelten Sprüngen - auch tatsächlich zu
sehr schwer lesbaren Code (Spaghetti-Code). Mitunter kann der Einsatz von goto
aber auch sinnvoll sein, um z.B. mehrere ineinander verschachtelte
Schleifen mit einer einzigen Anweisung zu verlassen.
goto
muss eine Marke angegeben werden, zu der gesprungen werden soll. Diese Marke muss sich im gleichen Gültigkeitsbereich befinden.
switch
-Anweisung kann goto
verwendet werden, um zu einem case
- oder dem default
-Zweig zu springen.
Das Beispiel gibt so lange das Produkt der Zahlen i
und j
aus, bis es größer als 30 wird. In diesem Fall werden beide for
-Schleifen verlassen
und die Programmausführung wird mit der Anweisung nach der Marke Ende
fortgesetzt.
for(int i = 1; i < 11; i++) { for(int j = 1; j < 11; j++) { if((i * j) > 30) goto Ende; else Console.WriteLine(i * j); } } Ende: Console.WriteLine("Fertig");
Präprozessordirektiven
.
#if
, #else
, #elif
und #endif
zur Kontrollflusssteuerung sowie die Direktiven #define
und #undef
zur
Definition bzw. Aufhebung eines Symbols. Über die Direktive #define
definieren Sie ein Symbol, das keinen bestimmten Wert besitzt. Es geht lediglich darum, dass das Symbol definiert
ist. Es besitzt damit implizit den Wert true
.
#define TESTDer Name des Symbols orientiert sich am Aufbau eines allgemeinen Bezeichners. Mit #undef machen Sie die Definition eines Symbols wieder rückgängig, z.B.
#define TEST ... #undef TESTDiese beiden Direktiven müssen sich am Anfang einer Datei, noch vor allen anderen Anweisungen befinden, ansonsten meldet der Compiler einen Fehler. Nachdem ein oder mehrere Symbole definiert sind, kann deren Existenz durch weitere Direktiven im Code geprüft werden. Mittels der Direktive
#if
prüfen Sie, ob ein Symbol definiert ist. Die folgenden
Anweisungen werden nur dann vom Compiler berücksichtigt, wenn der Test mit #if
erfolgreich war. Der "Anweisungsblock" wird durch eine der Direktiven #endif
,
#else
oder #elif
beendet. Die Direktive #endif
beendet immer den gesamten Block, #else leitet eine Alternative ein und #elif
ist
eine Zusammensetzung aus #else if
. Mehrere Symbole können über die Operatoren ==
, !=
, &&
und ||
verknüpft, einzelne über !
negiert werden.
#if Test Console.WriteLine("Test"); #endif
Zum Testen einer Anwendung sollen spezielle Ausgaben durchgeführt werden. Außerdem soll eine einfache Möglichkeit geschaffen werden, diese Ausgaben wieder
zu deaktivieren. Mittels Direktiven werden sie nicht nur deaktiviert, sondern auch nicht vom Compiler berücksichtigt. Zu Beginn wird das Symbol TEST
definiert.
Um es zu deaktivieren, z.B. wenn es in einer anderen Datei oder in den Projektoptionen definiert wurde, verwenden Sie die darunter angegebene Direktive #undef
.
Über die Direktive #if
wird auf die Existenz des Symbols TEST
geprüft. Ist es definiert, wird die darauf folgende Ausgabe durchgeführt. Durch die
Angabe des #else
-Zweigs wird eine Ausgabe erzeugt, wenn das Symbol nicht definiert ist.
#define TEST // #undef TEST using System; namespace CSharpBuch.Kap04 { public class Direktiven { static void Main(string[] args) { #if TEST Console.WriteLine("TEST ist definiert."); #else Console.WriteLine("TEST ist NICHT definiert."); #endif } } }Listing 4.13: Direktiven.cs
#define
existiert eine Compileroption /define
, so dass Sie Symbole auch über die
Kommandozeile oder im Visual Studio über die Projektoptionen (Menüpunkt , Register ERSTELLEN,
Eingabefeld SYMBOLE FÜR DIE BEDINGTE KOMPILIERUNG) definieren können.#region
und #endregion
eingeschlossen. Hinter der Direktive #region
kann optional eine Beschreibung des Codeblocks angegeben werden. Diese wird nach dem Zusammenklappen anstelle des Codeblocks angezeigt.
Die Verwendung von Regionen hat nur Auswirkungen in einer Entwicklungsumgebung, welche Regionen unterstützt. Auf den Compiler oder die erzeugte Anwendung hat dies keinen Einfluss.
#region Hier beginnt ein CodeBlock ... #endregion
#region
ein Symbol mit einem Minuszeichen darin, das einen aufgeklappten Codeblock kennzeichnet. Das Ende des Blocks wird durch
eine weitere Linie gekennzeichnet.
Abbildung 4.4: Definition einer Code-Region
Klappen Sie den Codeblock durch Klick auf das Symbol mit dem Minuszeichen zusammen, wird der Text hinter #region
anstelle des vollständigen Codes angezeigt.
Lassen Sie den Mauszeiger einen Moment über der Beschreibung stehen, wird darunter der zusammengeklappte Code in einem Infobereich angezeigt.
Abbildung 4.5: Zusammengeklappte Code-Region mit Vorschau
#warning
und #error
können Sie selbst Warnungen und Fehler ausgeben. Der entsprechende Text wird direkt hinter der Direktive angegeben.
Mittels der Direktiven #if
, #else
oder #elif
lassen sich diese auch abhängig von der Definition von Symbolen erzeugen. Während eine Warnung
lediglich eine Information ist, beendet ein Fehler tatsächlich die Übersetzung.
#if TEST #warning ACHTUNG ! TEST ist definiert !! #endifIn diesem Zusammenhang interessant ist auch die (De)Aktivierung von Warnmeldungen des Compilers. Dazu dient die Direktive #pragma warning. Dahinter geben Sie
disable
(deaktivieren) oder restore
(wiederherstellen) sowie die Nummer(n) der Warnungen an. Mehrere Nummern werden durch Kommata getrennt. Werden keine konkreten Warnungen angegeben,
werden alle Warnungen (de)aktiviert.
#pragma warning restore #pragma warning disable 100
Der folgende Ausschnitt deaktiviert eine Warnung, die daraus resultiert, dass die Variable i
nicht weiter verwendet wird: Das Feld "CSharpBuch.Kap04.Direktiven.i"
wird nie verwendet. Um diese Warnung zu deaktivieren, wird hinter die Direktive #pragma warning
die Angabe disable
sowie die Warnnummer angegeben.
#pragma warning disable 169 static int i; static void Main(string[] args) { ...Listing 4.14: Direktiven.cs
Abbildung 4.6: Nummer einer Warnmeldung ermitteln
unsafe
einschließen. Entweder Sie verwenden dazu einen Anweisungsblock oder
Sie kennzeichnen eine ganze Methode als unsicher.
unsafe { int *p; } unsafe void tueEtwas() { int *p; }Damit Sie unsicheren Code übersetzen können, müssen Sie beim Kompilieren die Option
/unsafe
angeben. Im Visual Studio müssen Sie in den Projektoptionen die Option
UNSICHEREN CODE ZULASSEN aktivieren (Menüpunkt , Register ERSTELLEN).
Abbildung 4.7: Unsicheren Code im Visual Studio aktivieren
Um die Umsetzung einer verwalteten Variablen im Speicher durch den Garbage Collector zu verhindern (was z.B. bei der Zusammenfassung von Speicherbereichen erfolgen kann), muss
diese als fixed
gekennzeichnet werden. Dies betrifft nicht den Zeiger, sondern die verwaltete Variable, auf die er zeigen soll. Mit fixed
gekennzeichnete Blöcke
sind nur in unsafe
-Blöcken möglich. Lokale Variablen befinden sich auf dem Stack und werden deshalb nicht vom Garbage Collector verwaltet, so dass diese nicht als fixed
gekennzeichnet werden müssen.
In einem unsicheren Block wird ein Zeiger auf die verwaltete Variable quelle1
erzeugt. Da die Variable den Wert 10 enthält, zeigt der Zeiger ebenfalls auf
diesen Wert. Wenn der Variablen ziel
über *zeiger1
der Inhalt des Zeigers zugewiesen wird, besitzt dann auch die Variable ziel
den Wert 10, d.h.,
es ist eine echte Kopie. Im zweiten Teil wird der gleiche Vorgang noch einmal mit einer Variablen quelle2
durchgeführt. Da es sich um eine lokale Variable handelt,
ist keine Angabe von fixed
notwendig.
using System; namespace CSharpBuch.Kap04 { public class Unsicher { static int quelle1 = 10, ziel; static void Main(string[] args) { unsafe { fixed(int* zeiger1 = &quelle1) { ziel = *zeiger1; Console.WriteLine("Ziel hat den Wert: " + ziel); } int quelle2 = 20; int* zeiger2; zeiger2 = &quelle2; ziel = *zeiger2; Console.WriteLine("Ziel hat jetzt den Wert: " + ziel); } } } }Listing 4.15: Unsicher.cs
switch
-Anweisung aus, führen Sie diese durch und geben Sie das Ergebnis aus.
for
-Anweisung die Ausgabe eines Countdowns. Dabei soll von 10 auf 1 heruntergezählt werden. Nach jeweils einer Sekunde Wartezeit erfolgt eine Ausgabe.
Um eine Sekunde Wartezeit zu realisieren, binden Sie zu Beginn den Namespace System.Threading
ein. Über die folgende Anweisung wartet die Anwendung 1000 Millisekunden und wird dann weiter ausgeführt.
Thread.Sleep(1000);Das Ergebnis könnte z.B. so aussehen:
Abbildung 4.8: Realisierung eines Countdowns