1 C# Grundlagen
1.1 C# Compiler
Im folgenden werden die Stufen der Übersetzung
eines C#- Programms dargestellt:
1.1.1.1 Übersetzen auf der Kommandozeile
Im Folgenden wird ein einfaches Programm erstellt,
welches auf der Kommandozeile den String "Hallo Welt"
ausgibt. Anhand des Programmes wird die Kompilation in MSIL sowie
das Kompilat selbst untersucht.
1.1.1.1.1 Beispiel Hallo Welt
Legen sie eine Datei namens Hallo.cs an.
=>
Quelltexte werden in Unicode- Dateien mit der Endung cs
gespeichert.
// C# Kennenlernen
// Kommentare sind einzeilig und werden mit einem Hochkomma (') eingeleitet
// Alle Prozeduren und Funktionen müssen in einem Modul-
// oder Klassenblock eingeschlossen werden
class CHallo {
// Die Prozedur Main hat eine besondere Bedeutung: Beim Programmstart
// wird vom Lader des Betriebssystems an diese die Kontrolle übergeben
public static void Main() {
// Hier wird ein Unterprogramm aus der Bibliothek System.dll aufgerufen
// Mittels des Unterprogramms Console.WriteLine erfolgt die Ausgabe
// der Zeichenkette "Hallo Maja" auf der Kommandozeile
System.Console.WriteLine("Hallo Maja");
}
}
1.1.1.1.2 Übersetzen
Das Beispielprogramm kann auf der Kommandozeile
(Visual- Studio- Tools/Command Prompt) durch folgenden Befehl
kompiliert werden:
c:\cs-kurs\csc hallo.vb /reference:System.dll
Die Option /reference informiert den
Compiler , das aus der System.dll Metainformationen zu
beziehen sind.
1.1.2 Kommentare
In einer Quelltextdatei können zum Zwecke der
Dokumentation Zeilen von der Kompilation ausgeschlossen werden. Man
nennt diese Abschnitte auch Kommentare.
Es gibt einzeilige
// Ein Kommentar: einzeilige Kommentar
und Mehrzeilige Kommentare
/* Ein weiterer Kommentar:
mehrzeiliger Kommentar */
1.1.3 Präprozessor
Mittels Präprozessordirektiven können
Abschnitte von der Kompilation ausgeschlossen werden.
1.1.3.1.1 #define
Mit #define kann eine Konstante definiert
werden, die in Präprozessorbefehlen für die bedingte
Kompilation ausgewertet wird. Die Konstanten müssen als erste
Anweisungen in einer Quelltextdatei definiert werden.
#define XX_GRUSS_MAJA
Class CHallo
public static void Main() {
:
}
}
Hier wird eine Konstante namens XX_GRUSS_MAJA deklariert.
1.1.3.1.2 #if (...) #else
In Abhängigkeit
von bedingten Konstanten können Abschnitte im Quelltext von der
Kompilation ausgeschlossen werden oder nicht:
#define XX_GRUSS_MAJA
#define XX_KEIN_GRUSS
class CHallo {
public static void Main() {
#if (XX_GRUSS_MAJA && !XX_KEIN_GRUSS)
System.Console.WriteLine("Hallo Maja");
#elif (!XX_GRUSS_MAJA && !XX_KEIN_GRUSS)
System.Console.WriteLine("Hallo Marina");
#endif
#if (XX_KEIN_GRUSS)
#error Kein Gruss
#endif
}
}
In diesem Beispiel wird die Zeile
zwischen #if ... #endif nicht kompiliert. Beim kompilieren wird zudem
die Fehlermeldung Kein Gruss generiert. Wenn die zweite Zeile
(#define XX_KEIN_GRUSS) auskommentiert wird, dann erfolgt keine
Kompilation für die Zeile zwischen #if ... #else.
1.1.3.1.3 # region
Dient zur logischen Gliederung des Quelltextes.
Die IDE kann Regionen wahlweise auf- und zuklappen. Bsp.:
#region "mm"
void p1 {
...
}
void p2 {
...
}
#end region
1.1.4 Testausgabe mittels Debug und Trace Klassen
Bei der Entwicklung von Programmen muss der
Programmierer oft das Verhalten zur Laufzeit überwachen. Dazu
macht er hilfsweise Ausgaben von aktuellen Variablenwerten, dem
Ergebnis von Berechnungen etc.. Dazu bietet das .NET Framework ein
ideales Instrument, die Debug-
und Trace Klasse.
Die Debug- Klasse
bietet Unterprogramme zur testweisen Ausgabe und zum testweise prüfen
von Werten an:
// Testweise Ausgabe des Wertes x im Direkt/Ausgabefenster von VS2012
Debug.WriteLine("x= " + x);
// Testweise überprüfung, ob x größer 0 ist. Wenn nicht,
// dann wird eine Ausnahme ausgelöst
Debug.Assert(x > 0);
Wie die Präprozessorbefehle
können alle Unterprogrammaufrufe von Debug über einen
Compilerschalter wahlweise aus dem Quelltext vor der eigentlichen
Übersetzung entfernt werden. Die Schalter sind die Debug-
und Trace-
Preprozessorkonstanten.
1.2 Grundlegende Sprachelemente in C#
1.2.1 Literale und Namen
1.2.1.1 Literale
Literale gehören zu den Grundbausteinen einer
Programmiersprache und bezeichnen die Vereinigung der Mengen aller
Standard in C# ist die Darstellung von
Zahlenwerten im dezimalen Zahlensystem (Basis 10). Der Wert von
logischen Aussagen kann durch die beiden Elemente true oder
false ausgedrückt
werden.
Achtung:
Für Datumswerte gibt es in C#
keine Literale. Datumswerte werden vollständig durch die .NET-
Klasse DateTime abgebildet.
Menge
|
Beispiel
|
Anmerkung
|
ganze Festkommazahlen, dezimal
|
123
|
|
ganze Festkommazahlen, Hexadezimal
|
0xAF
|
|
negative Festkommazahlen
|
-123
|
|
vorzeichenlose Festkommazahlen
|
4000000000u
|
Durch das Postfix u wird jede Festkomma
Konstante in einen vorzeichenlosen Typ gewandelt (z.B. ushort x =
40000u)
|
Gleitkommazahl (doppelt genau, 64bit, ca. 15
Stellen)
|
123.4
oder 123.4d
|
|
Gleitkommazahl (einfach genau, 32bit, ca. 8
Stellen)
|
123.4f
|
|
Gleitkommazahl Zahl (super genau)
|
1234567890123456789012345m
|
Die Werte werden nicht mehr direkt durch CPU-
Befehle verarbeitet, sondern durch eine Bibliothek.
|
Gleitkommazahlen in Exponentialschreibweise
|
1.23e4
|
|
Zeichen
|
'A'
|
|
Zeichen als Unicode- Referenz (hexadezimal)
|
'\u0058'
|
Escapesequenz \u als Präfix für
Unicode- Nummer
|
Zeichenketten
|
"ABC\n"
|
In Zeichenkettenliterale werden
Escapesequenzen wie \n durch die entsprechenden ASCII-
Steuerkodes wie CF+LF ersetzt. Soll ein \ ausgegeben werden, dann
muss er durch die besondere Escapesequenz \\ dargestellt werden.
|
literale Zeichenketten
|
@"\s+XYZ"
|
In literalen Zeichenketten findet keine
Ersetzung von Escapesequenzen statt. Sie sind damit besonders zur
Darstellung von regulären Ausdrücken geeignet.
|
Wahrheitswerte
|
true oder false
|
|
Nullwert
|
null
|
|
1.2.1.2 Regeln für Namen
Für
die Benennung von Komponenten eines Programms wie Variablen oder
Unterprogramme gelten in C# wie in vielen anderen Programmiersprachen
folgende Einschränkungen.
Erste
Zeichen muss ein Buchstabe sein:
Richtig:
A1
Falsch:
1A
Der
Name darf keine Leerraumzeichen enthalten
Richtig:
neuerMitarbeiter
Falsch:
neuer Mitarbeiter
Es sind nur
Buchstaben, Zahlen und der Unterstrich _ als Zeichen erlaubt
Richtig:
Preis_in_Euro
Falsch:
Preis_in_€
Der
C# Compiler verarbeitet Unicode- Quelltextdateien. Damit sind auch
Variablennamen mit z.B. Umlauten zulässig
double akt_Ölpreis_in_Dollar = 100.0;
Man sollte jedoch
bedenken, dass in internationalen Programmierteams Mitglieder, die
keine Deutsche Tastatur besitzen, bei der Pflege solcher Quelltexte
unnötigen Aufwand haben.
1.2.2 Blöcke
Die elementarste Struktur aller
Programmiersprachen sind Blöcke. Ein Block ist ein
zusammenhängendes Stück Text (Programmcode), das durch
spezielle Schlüsselworte abgegrenzt wird.
Blockbeginn Schlüsselwort
Programmtext
Blockende Schlüsselwort
Auch C# ist eine block- strukturierte Programmiersprache. Jedes C#
Programm ist eine Hierarchie aus Blöcken. Alle
Variablendeklarationen, Anweisungen etc. müssen immer innerhalb
eines Blockes notiert werden.
Jeder Block wird durch ein Paar geschweifter
Klammern gebildet:
{ /* Blockinhalt */ }
1.2.2.1 Verschachtlung
Die Anweisungslisten innerhalb von Blöcken
können wieder in Blöcke eingeschlossen werden, so dass eine
Baumstruktur entsteht. Man spricht von Verschachtlung.
{
// Äußerer Block
{
// Innerer Block
}
}
Alle anderen Varianten sind
verboten. So können sich z.B. zwei Blöcke nicht
überschneiden. Die Interpretation einer Blockstruktur wie folgt
wäre falsch !
{ /* Block 1 */ { /* Block 1 + Block2 */ } /* Block 2 */ }
Die richtige Interpretation
lautet:
{ /* Block 1 */ { /* Block2 */ } /* Block 1 */ }
1.2.2.2 Blocktypen
Blöcke werden bezüglich ihres Inhaltes
klassifiziert. So gibt es Blöcke, die den Code von
Unterprogrammen und Funktionsimplementierungen enthalten, andere
umschließen Mengen von Namen.
Der Typ eines Blockes wird in C# vor der öffnenden
Klammer notiert:
[Blocktyp [Blockname]]
{ /* Blockinhalt */ }
Im Folgenden ein Auszug der
Blocktypen von C#
Blocktyp
|
Beschreibung
|
namespace
|
Umschließt eine Menge von Namen (z.B.
Variablennamen). Zweck ist es, die Homonyme in großen
Programmsystemen zu vermeiden.
|
class
|
Der Block definiert eine Klasse (= Container
für Anwendunglogik)
|
foreach
|
Der Block enthält eine Liste von
Anweisungen, die auf jedes Element einer Menge anzuwenden sind.
|
1.2.2.3 Namespaces
Namespaces dienen zur Abgrenzung der Namensmengen
zwischen verschiedenen Teilen von Bibliotheken und Programmen.
namespace outer {
namespace inner {
public int x;
}
}
// Zugriff auf x
outer.inner.x = 99;
1.2.2.3.1 using- Direktive
Der Zugriff auf Typen und Funktionen , die in tief
verschachtelten Namensräumen definiert sind, kann zu einem sehr
unübersichtlichen Code führen. Beispiel:
namespace mko.Algo.Statistics
{
public static class Fn
{
/// <summary>
/// Summenbildung
/// Summe({a1, a2, …, aN}): {a1, a2, …, aN} → a1 + a2 + … + aN
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
public static double Sum2(System.Collections.Generic.IEnumerable<double> list)
{
if (mko.Algo.Listprocessing.Fn.Count(list) == 0)
return 0;
else if (mko.Algo.Listprocessing.Fn.Count(list) == 1)
return mko.Algo.Listprocessing.Fn.First(list);
else
return mko.Algo.Listprocessing.Fn.First(list) + Sum(mko.Algo.Listprocessing.Fn.Skip(list, 1));
}
}
Der Zugriff auf die Funktionen
aus mko.Algo.Listprocessing, als auch auf die Typen aus
System.Collections.Generic verursacht sehr lange Namen. Abhilfe
schafft hier die using. Direktive. Diese definiert einen Eintrag in
einer Tabelle, auf die der Compiler zugreift, wenn er auf einen Namen
beim Übersetzen trifft, den er nicht auflösen kann. Es
ergeben sich 3 Möglichkeiten:
Es gibt
einen Eintrag in der Tabelle, die als Namensraum vor den
unauflösbaren Namen gesetzt werden kann, so dass der Namen nun
voll qualifiziert auflösbar ist
Es gibt
einen Eintrag, der den unauflösbaren Namen als Alias für
einen Namensraum definiert
Es gibt
einen Eintrag, der den unauflösbaren Namen als Typ- Alias für
einen voll qualifizierten Typnamen definiert
using Namespace;
using Alias = Namespace;
using TypeAlias = LongTypeName
Im folgenden wird das obige
Beispiel mittels using- Direktiven deutlich kürzer und lesbarer
using lisp = mko.Algo.Listprocessing;
// Für C++ Prog.: folgende Anweisung entspricht einem typedef in C++
using Ldouble = System.Collections.Generic.IEnumerable<double>;
namespace mko.Algo.Statistics
{
public static class Fn
{
/// <summary>
/// Summenbildung
/// Summe({a1, a2, …, aN}): {a1, a2, …, aN} → a1 + a2 + … + aN
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
public static double Sum(Ldouble list)
{
if (lisp.Fn.Count(list) == 0)
return 0;
else if (lisp.Fn.Count(list) == 1)
return lisp.Fn.First(list);
else
return lisp.Fn.First(list) + Sum(lisp.Fn.Skip(list, 1));
}
}
1.2.2.4 Klassenblöcke
Jegliche Form von Anwendungslogik wie
Datenspeicher (Variablen) sowie in Funktionen und Unterprogrammen
verpackte Algorithmen müssen in Klassenblöcke
eingeschlossen werden.
Ein Klassenblock wird mit dem Schlüsselwort
class ausgezeichnet. Jeder
Klassenblock wird zusätzlich mit einem innerhalb des Namespaces
eindeutigen Namen benannt.
class MeineKlasse {
// Hier werden Variablen, Unterprogramme und Funktionen definiert
// Diese werden auch vrallgemeinert als Member einer Klasse bezeichnet
}
Die in einem Klassenblock eingeschlossenen Strukturen werden Member
genannt.
1.2.2.5 Unterprogrammblock
Algorithmen werden in C# in Unterprogramm oder
Funktionsblöcken verpackt. Hier wird zunächst die
einfachste Form eines Unterprogrammblockes eingeführt. Dieser
kann in den ersten Übungen mit C# zum Testen von im folgenden zu
besprechenden Ausdrücken und Anweisungen benutzt werden:
static void MeinUnterprogramm()
{
// Hier steht die Liste aus Anweisungen, welche Algorithmen implementiert
}
1.2.2.6 Alles zusammen …
… ergibt den notwendigen Rahmen, innerhalb
dessen man mit dem Programmieren anfangen kann:
namespace MeinNamespace {
class MeineKlasse {
static void MeinUnterprogramm() {
// Meine Liste aus Anweisungen
}
}
}
1.2.3 Anweisung
Ein Programmtext besteht in C# aus Anweisungen.
Der Computer führt die Anweisungen aus, und bewirkt damit
Zustandsänderungen in sich.
Syntaktisches Merkmal einer Anweisung ist, dass
sie mit einem Semikolon abgeschlossen wird:
Anweisung;
Grob können Anweisungen in drei Klassen aufgeteilt werden:
Deklarationen
|
Zuweisungen
|
Sprunganweisungen
|
Anweisungen an den Compiler,
Entwurfszeitobjekte (z.B. Delegaten) und Laufzeitobjekte (z.B.
Variablen) anzulegen
// Entwurfszeitobjekt
delegate int F();
// Laufzeitobjekte
F fkt = () => 99;
int A;
|
Verändern den Wert im Arbeitsspeicher
A = 3;
|
Setzen den Befehlszähler auf einen neuen
Wert, so dass ein Programm an einer anderen Stelle fortgesetzt
wird.
goto SPRUNGMARKE;
|
Anweisungen können komplexe Ausdrücke
(siehe weiter unten) enthalten. Vor Ausführung der Anweisung
müssen die Ausdrücke evaluiert werden:
A = 3 * (B + 99);
1.2.3.1 Deklaration
Eine Deklaration weist den Compiler an, Code zu
erzeugen, um bestimmte Strukturen und Objekte für die Laufzeit,
als auch Entwurfszeit bereitzustellen.
Mittels einer Variablendeklaration kann z.B. ein
Stück Speicherplatz reserviert, und mit einem Namen versehen
werden:
// Variable A wird angelegt, und mit 0 initialisiert
int A;
1.2.3.2 Zuweisung
Eine Zuweisung ist eine spezielle C#-
Anweisung der Form:
Variable = Ausdruck;
Sie entspricht einem Kopierbefehl, wobei der evaluierte Ausdruck
rechts neben dem Gleichheitszeichen an den benannten Speicherplatz
(Variable) links neben den Gleichheitszeichen kopiert. Im Folgenden
Beispiel wird in die Variable A der Wert 99 kopiert:
A = 99;
1.2.3.3 Sprunganweisungen
Der direkte Einsatz von Sprunganweisungen ist seit
Erfindung der Strukturierten Programmierung verpönt. Den ihre
extensive Anwendung führt zu sehr unübersichtlichen
Programmabläufen- solche Programme sind kaum wartbar.
Trotzdem bietet auch das moderne C# eine direkte
Sprunganweisung an:
A = 99;
goto SPRUNGMARKE;
A = 100;
SPRUNGMARKE:
B = A;
Nach Ausführung dieser Anweisungen wird B immer den Wert 99
haben, denn die Anweisung A = 100 wird übersprungen.
Sinn macht das goto im modernen C#, wenn der
Praktiker z.B. die Aufgabe bekommt, in einer existierenden
Programmstruktur Fehler zu finden, und es dabei notwendig wird, z.B.
tief verschachtelte Blöcke vorzeitig zu verlassen, um Teile der
Logik separat testen zu können:
if( … ) {
…
if( … ) {
…
// goto nur zum Debuggen von Laufzeitfehlern
goto MeineDebugsprungmarke;
…
}
…
}
MeineDebugsprungmarke:
Im Programmieralltag wird der Sprungbefehl z.B. versteckt in einem
bedingten Anweisungsblock benutzt. Dieser evaluiert am Blockanfang
einen Ausdruck zu einem der Wahrheitswerte true oder
false. War das
Ergebnis true, dann wird normal fortgesetzt, sonst wird zum Blockende
gesprungen:
// Einkommensteuer wird nur berechnet, wenn das Einkommen über dem
// Steuerfreibetrag liegt
Steuer = 0;
if(Einkommen > Steuerfreibetrag) {
// Einkommensteuer berechnen
Steuer = Einkommen * …
}
// Ausgabe der zu entrichtenden Steuern
Debug.WriteLine(Steuer);
1.2.4 Ausdrücke
Ein Ausdruck ist in C# ein Konstrukt aus Namen,
Werten, Operatoren und Funktionsaufrufen, das zu einem Wert
ausgewertet werden kann. Die Auswertung wird als Evaluierung
bezeichnet.
Beispiele:
3 // wird zu 3 ausgewertet
3 + 4 // wird zu 7 ausgewertet
(3 + 4) * 2 // wird zu 14 ausgewertet
D // Auswertung liefert den Inhalt der Variable D
Math.Pi * D // wird zum Kreisumfang ausgewertet, wenn in über Variable D
// der Durchmesser bereitgestellt wird.
1.2.4.1 Vergleich auf Identität
In der Mathematik steht
das Gleichheitszeichen für die Identität zwischen rechter
und linker Seite vom Gleichheitszeichen. In C# wird ein solcher
Sachverhalt durch das doppelte Gleichheitszeichen in einem Ausdruck
dargestellt. Das einfache Gleichheitszeichen steht für die
Zuweisung = Kopierbefehl (siehe unten) !
Ausdruck_A == Ausdruck_B // liefert true, wenn evaluierter Ausdruck_A identisch
// evaluiertem Ausdruck_B ist
1.2.4.2 Funktionsaufrufe
Eine Funktion bildet eine Liste von Eingangswerten
aus einen Ausgangswert ab:
E1 --+
|
Eingänge E2 --+-Funktion_f-> Ausgangswert
… |
En --+
Ein funktionale Abbildung wird in
C# durch spezielle Ausdrücke, den Funktionsaufrufen
implementiert:
Funktionswert == Funktion_f (E1, E1, …, En);
| |_____________|
| |
Funktions- Parameterliste
name
In C# gibt es bereits viele
eingebaute Funktionen. Beispiele:
' Eingabaute Funktionen
static void TestBuildInFunctions() {
double y = 0.0;
// Sinusfunktion (Mathe, Trigonometrie)
y = Math.Sin(Application.Pi / 2);
Debug.Assert (y = 1.0);
string anzZeichen = "";
// Zeichenkettenfunktion. Eingegebener Text wird auf die Anzahl
// der Zeichen des Textes abgebildet
anzZeichen = string.Length("Hallo Welt");
Debug.Assert (anzZeichen = 10);
}
1.2.4.3 Bedingte Ausdrücke (Ternärer Operator)
Ein bedingter
Ausdruck ist eine spezielle VB.NET- Funktion mit drei Parametern:
Ergebniswert == Bedingung ? ExprA : ExprB;
Zuerst wird die Bedingung evaluiert. Ergibt sie den Wert true,
dann wird ExprA evaluiert und deren Wert entspricht dem Ergebniswert.
Sonst entspricht der Evaluierte Wert von ExprB dem Ergebniswert.
1.2.5 Operatoren
Operatoren sind grundlegende Abbildungen von
Mengen in sich selbst. Beispielsweise ist die Addition zweier
Festkommazahlen ein Operator.
Eine Anweisung kann mehrere Operatoren enthalten
wie:
A = 3 * (B + 99)
In diesem Falle muss bei der Ausführung der Anweisung über
die Reihenfolge entschieden werden, in der die Operatoren anzuwenden
sind. Basis dafür ist die Priorität der Operatoren.
Operatoren mit höherer Priorität werden vor Operatoren mit
niedrigerer Priorität ausgeführt.
C# liefert eine Umfangreiche
Menge an Operatoren wie bei C- artigen Sprachen üblich. Die
Menge der Operatoren kann durch selbstdefinierte erweitert werden
durch Operator
overloading.
1.2.5.1 Geprüfte und Ungeprüfte arithmetische
Festkomma- Operationen
try {
int x = int.MaxValue;
checked {
x++;
}
} catch (OverflowException ex) {
Console.WriteLine(ex.Source + "hat einen Überlauf veursacht");
}
1.2.6 Variablen
Variablen sind benannte Speicherplätze.
In die Speicherplätze wird geschrieben oder aus ihnen wird
gelesen, indem auf diese der Zuweisungsoperator = mit einem Wert
angewendet wird:
// Speichern des Wertes 3.14 in meineVariable
meineVariable = 3.14;
// Lesen eines Wertes aus meinerVariable und speichern in deinerVariable
deineVariable = meineVariable;
1.2.6.1 Deklaration von Variablen
In jedem C# Programm muss eine Variable vor ihrer
Verwendung deklariert werden. Dabei wird dem Compiler der
Variablenname, der Typ der Daten, die unter der Variable
abzuspeichern sind, sowie Zugriffsbeschränkungen
(Zugriffsmodifikator) und Lebensdauerbeschränkungen
(Modifikator) verbindlich mitgeteilt.
[Zugriffsmodifikator] [Modifikator] Typ Variablenname [ = Initialwert];
Der Zugriffsmodifikator steuert den Zugriff auf eine Variable
innerhalb ihres Gültigkeitsbereiches. Durch den Typ wird
festgelegt, welchen Datentyp die Variable hat.
Mögliche
Modifikatoren: const, readonly, static
Mögliche
Zugriffsmodifikatoren: private, protected, public, internal
1.2.6.2 Initialisierung
Für Variablen, die Member einer Klasse sind,
erfolgt eine automatische Initialisierung. Lokale Variablen (=
Variablen, die innerhalb von Unterprogrammen/ Funktionen deklariert
werden) müssen manuell initialisiert werden:
class MeineKlasse {
// Membervariablen können, müssen nicht explizit initialisiert werden:
int MeineMembervariable1; // implizit mit 0 initialisiert
int MeineMembervariable2 = 99; // explizit mit 99 initialisiert
void MeinUnterprogramm() {
// lokale Variablen müssen deklariert werden
int meineLokaleVariable = 0;
}
}
1.2.6.3 Sichtbarkeit, Lebensdauer und Gültigkeitsbereich von Variablen
Die Blockstruktur dient nicht nur zur Gliederung
eines C# Programms. Mit der Blockstruktur wird auch die Lebensdauer,
die Sichtbarkeit und die Gültigkeit einer
Variablen innerhalb eines Programms geregelt.
Achtung:
Im Bild sind bei der Darstellung des Stapels "unten"
und "oben" vertauscht. Die Darstellung folgt dabei der
Implementierung von Stapeln im Arbeitsspeicher, bei denen der Stack
an der höchsten Adresse beginnt, und zu niederen Adressen hin
wächst. Die höchstwertige Adresse ist dabei "unten".
Die Lebensdauer einer
Variable ist von der Position auf dem Stack abhängig. Je weiter
unten, desto länger "lebt" eine Variable.
Die
Sichtbarkeit
einer Variable wird durch den
Gültigkeitsbereich und
durch Namensvetter weiter oben auf dem Stapel beeinflusst.
Globale Variablen wie
statische Membervariablen einer Klasse sind für das gesamte
Programm gültig. Für lokale Variablen
hingegen ist der Gültigkeitsbereich
auf den unmittelbaren
Unterprogrammblock beschränkt.
Gibt es zu einer
globalen Variable weiter unten im Stapel einen gültigen
Namensvetter weiter oben, dann wird die globale Variable durch ihren
lokalen Namensvetter weiter oben überdeckt.
Nur Variablen auf dem
Stapel, die nicht überdeckt werden und gültig sind, sind
auch sichtbar.
1.2.6.3.1 Statische Variablen
In Sonderfällen kann es sinnvoll sein, die
Lebensdauer von Variablen in Funktionen und Prozeduren über die
Blockgrenzen hinaus zu verlängern. Soll z.B. protokolliert
werden, wie oft eine Methode in einem Programm schon aufgerufen
wurde, dann muss eine Zähler- Variable bereitgestellt werden,
die bereits zu Programmstart initialisiert und erst bei Programmende
vernichtet wird. Erreicht wird dies durch den Zugriffsmodifizierer
static. Statische Variablen dürfen in C# nur auf der
Ebene von Klassendeklarationen (= Membervariablen) vereinbart werden:
class CUtils {
static long id = 0;
static long MakeID() {
id += 1;
return id;
}
}
Achtung: Statische Methoden können nur auf statische
Variablen, die als Member definiert wurden, zugreifen.
1.2.7 Elementare Datentypen
Der Datentyp
steht für eine Klasse von Daten. Beispielsweise verbirgt sich
hinter dem Datentyp short die Menge aller ganzen Zahlen im
Intervall [-32768, 32767]. Der Datentyp bestimmt, wie die
Informationen als Binärzahlen im Speicher zu verschlüsseln
sind, und wie viel Speicherplatz für die Variable benötigt
wird. So werden Variablen vom Typ short vom Dezimal- ins Dualsystem
konvertiert, und für sie ein Speicherplatz von 16bit = 2Byte
reserviert.
Die primitiven Datentypen von C# werden durch die
Common Type Specification (CTS) definiert, die für alle
.NET Sprachen gilt. Durch einen einheitlichen Satz primitiver
Datentypen für alle Programmiersprachen wird die
Interoperabilität garantiert.
C# hält für viele CTS- Typen
Aliasbezeichner bereit. Hierdurch werden die CTS- Typen in den
"Kulturkreis der C- Sprachen" eingebettet.
Da alle .NET Sprachen streng objektorientiert
sind, und auch Variablen primitiven Typs in bestimmten Situationen
wie Objekte behandelt werden müssen, gibt es im Namespace System
für alle primitiven
Datentypen jeweils eine Datenstruktur.
C#
|
CTS
|
Wertebereich
|
Literale
|
Anmerkungen
|
bool
|
System.Boolean
|
{true, false}
|
true
|
Wertetyp
|
sbyte
|
System.SByte
|
[-128, 127]
|
99
|
Wertetyp
|
byte
|
System.Byte
|
[0, 255]
|
255
|
Wertetyp
|
char
|
System.Char
|
[0, 65535]
|
'A'
|
Wertetyp
|
short
|
System.Int16
|
[-32768, 32767]
|
199
|
Wertetyp
|
ushort
|
System.UInt16
|
[0, 65535]
|
199
|
Wertetyp
|
int
|
System.Int32
|
[-2147483648, 2147483647]
|
199
|
Wertetyp
|
uint
|
System.UInt32
|
[0, 4294967295]
|
199 oder 199U
|
Wertetyp
|
long
|
System.Int64
|
[-9223372036854775808, 9223372036854775807]
|
199 oder 199L
|
Wertetyp
|
ulong
|
System.UInt64
|
[0, 18446744073709551615]
|
199 oder 199U
|
Wertetyp
|
float
|
System.Single
|
[-3.402823E+38, 3.402823E+38]
|
3.14 oder 3.14F
|
Wertetypen
Gleitpunktliterale sind per Default vom
Typ double
Division durch 0 führt zu Werten wie
+unendlich, -unendlich und NAN
|
double
|
System.Double
|
[-1.79769313486232E+308,
1.79769313486232E+308]
|
9.0 oder 9D
|
decimal
|
System.Decimal
|
[0, 79.228.162.514.264.337.593.543.950.335]
|
125.99M
|
|
string
|
System.String
|
0- 2 Mrd. Unicodezeichen
|
"Hallo Welt"
|
Referenztyp
|
-
|
System.DateTime
|
00001-1-1 0:0:0 bis 9999-12-31 23:59:59
|
|
Referenztyp
|
object
|
System.Object
|
|
|
Referenztyp
|
1.2.7.1 Enumeration als Teilmenge von int
Endliche Mengen lassen sich durch Aufzählungen
modellieren (Enumeration). Beipiel:
enum enumLaengenEinheiten {mm, cm, dm, m, km, AE, LichtJahr}
Enumerationen sind Teilmenge vom
Typ int. Die Symbole in einem enum- Block werden vom Compiler
automatisch durchnummeriert, beginnend bei 0. Die Nummerierung kann
auch vom Programmierer erfolgen, wie der folgende Auszug aus der
Beispielbibliothek BatchProcessing zeigt:
//----------------------------------------------------------------------------
//Zustände eines Jobs
public enum JobStates
{
undefined = 0x1, // Job wurde erstellt, aber es ist noch keine JobId
// definiert worden
defined = 0x2, // Job wurde neu erstellt und JopId ist definiert
waiting = 0x4, // Job befindet sich in der Warteschlange vor der
// Bearbeitungsstation
processing = 0x8, // Job wird bearbeitet
pause = 0x9, // Bearbeitung des Jobs pausiert/wurde kurz unterbrochen
finished = 0x10, //Job wurde fertiggestellt
aborted = 0x20, //Bearbeitung des Jobs wurde abgebrochen
}
Der Zugriff auf einen Enum- Wert
erfolgt über den Namen des Enums:
mJobState = JobStates.pause;
1.2.7.2 Typinferenz (ab NET 3.5)
Mit Typinferenz wird ein Verfahren bezeichnet, bei
dem aus dem Initial-wert einer Variable der Typ abgeleitet wird.
Typinferenz wurde im Zusammenhang mit Linq
(Language integrated Query) eingeführt, da der Datentyp
des Ergebnisses einer Linq- Abfrage sich häufig erst aus
der Abfrage ergibt.
Typinferenz sollte nur in Ausnahmefällen
eingesetzt werden, schließlich ist die Wahl eines Datentypen
für eine Variable eine Entscheidung, die bewusst vom
Programmierer getroffen werden muss.
// Typinferenz: Deklaration ohne Typspezifikation-> Typ wird aus dem zugewiesenen Wert bestimmt
var i = 0;
var str = "Hallo Welt";
// i ist nach wie vor streng typisiert
i = "Hallo Welt"; // => Fehler, da i vom Typ int
1.2.7.3 Typkonvertierung
1.2.7.3.1 Implizite Typkonvertierung
Implizite Typkonvertierungen sind zwischen
verwandten Datentypen möglich, wenn der Zieltyp mehr
Informationen aufnehmen kann als der Quelltyp:
1.2.7.3.2 Explizite
Typkonvertierung
Explizite Typkonvertierungen erzwingen die
Umwandlung von einem Typ in einen anderen. Dabei gibt es jedoch
verschieden starke Konvertierungsoperatoren. Auf der einen
Seite gibt es die C#- Konvertierungsoperatoren, auf der anderen Seite
die Operatoren der Klasse Convert.
Syntax der Konvertierungsoperatoren:
(Typname) Objekt
Beipspiel:
decimal decPi = 3.1415926535897932384626433832795M;
float fltPi = (float) decPi;
Die Convert Funktionen sind am stärksten
Konvertierungsfunktionen. Sie ermöglichen eine Konvertierung
zwischen an sich inkompatibelen Typen wie Zahlenwerte in Strings in
nummerische Werte von Int32 etc.
string strPreis = "199,00";
decimal decPreis = Convert.ToDecimal(strPreis);
1.2.7.3.3 Konvertierungsfehler
Ist eine Explizite Konvertierung nicht möglich,
dann wird eine Ausnahme vom Typ InvalidCastException geworfen:
try {
string txt = "hallo";
object refX = txt;
// Folgende Konvertierung führt zu einer Ausnahme
int x = (int) refX;
} catch (InvalidCastException ex) {
Console.WriteLine(ex.Message);
}
1.2.7.3.4 Konvertieren mittels as
Referenztypen können in C# mittels des as –
Operators konvertiert werden. Dieser löst im Falle einer
nichtdurchführbaren Konvertierung keine Ausnahme aus, und
entspricht damit in seiner Wirkungsweise folgendem Code:
refX is TypeX ? (TypeX)refX : null;
Beispiel:
string txt = "Hallo";
object refX = txt;
Exception ex = refX as Exception;
1.2.8 Strukturierte Programmausführung
mit Anweisungsblöcken
In den 1970-er Jahren
wurde erkannt, das Programme als reine Befehlsfolgen ab einer
bestimmten Größe nicht mehr wartbar sind, da sie schlicht
und ergreifend niemand mehr durchschaut (Spaghetti Code). Den Ausweg
aus diesem Dilemma fand man mit einer hierarchischen Strukturierung
der Befehlsfolge durch Anweisungsblöcke.
Ein Anweisungsblock
umschließt eine Folge von Anweisungen. Durch den Einschluss im
Block wird die enthaltende Anweisungsfolge konfigurierbar und
wiederverwendbar. Z.B. können die Anweisungen im Block:
wiederverwendbar
gemacht werden (Unterprogramme)
Wiederholt
ausgeführt werden (Schleife)
Nur unter
bestimmten Bedingungen ausgeführt werden (Verzweigung)
1.2.8.1.1 Bedingt ausführbarer
Programmabschnitt
Der
bedingt ausführbare Programmabschnitt entspricht einer Wenn
... dann Regel.
if (Bedingung ) {
Anweisung 1;
Anweisung 2;
:
Anweisung n;
}
Trifft die Bedingung nicht zu, dann kann anschließend mit dem
Schlüsselwort Else ein Programmabschnitt als Alternative
formuliert werden:
if ( Bedingung ) {
Anweisung 1;
:
Anweisung n;
} else {
Anweisung n+1;
:
Anweisung n+m;
}
Gibt es mehrere Alternativen, die von Zusatzbedingungen abhängig
sind, dann können diese in ElseIf- Blöcken
formuliert werden:
if ( Bedingung ) {
Anweisung 1;
:
Anweisung n;
} else if ( Bedingung2 ) {
Anweisung n+1
:
Anweisung n+m
}
Ist die Ausführung mehrerer Programmabschnitte vom Wert eines
einzigen Ausdrucks abhängig, dann kann alternativ zu ElseIf eine
vereinfachte Schreibweise mittels Select Case Block gewählt
werden:
switch (Ausdruck) {
case Wert1:
Anweisung 1;
:
Anweisung n;
break;
case Wert2:
Anweisung n+1;
:
Anweisung n+m;
break;
case Wert3:
:
default: {
Anweisung
}
}
1.2.8.1.2 Bedingt wiederholbarer Programmabschnitt
Es handelt sich
hierbei um Programmabschnitte, die sooft wiederholt werden, solange
eine Bedingung zutrifft (wahr ist). Sie werden gewöhnlich als
Schleifen bezeichnet. Zwei elementare Formen der Schleifen
werden unterschieden: die abweisende, und die nichtabweisende
Schleife.
Bei
der abweisenden Schleife wird stets am Anfang des Programmabschnittes
geprüft, ob die Bedingung für eine Ausführung des
Abschnittes erfüllt ist. Wenn ja, dann wird der
Programmabschnitt ausgeführt, und am Ende der Befehlszähler
wieder auf die Startadresse des Programmabschnittes gesetzt. Gilt die
Bedingung weiterhin, dann erfolgt eine wiederholte Ausführung
des Programmabschnittes. Trifft die Bedingung nicht zu, dann wird der
Programmabschnitt übersprungen.
while(Bedingung) {
Anweisung 1;
Anweisung 2;
:
Anweisung n;
}
Bei der nicht abweisende Schleife wird die Bedingung erst am Ende des
Programmabschnittes überprüft. Trifft sie zu, dann wird der
Befehlszähler auf den Abschnittsanfang gesetzt, und dieser
wiederholt. Sonst wird mit der nachfolgenden Anweisung fortgesetzt.
Da die Bedingung erst am Ende des Abschnittes
überprüft wird, wird der Abschnitt immer mindestens einmal
ausgeführt.
do {
Anweisung 1
Anweisung 2
:
Anweisung n
} while (Bedingung);
1.2.8.1.3 for- Schleife
Die for- Schleife wird klassisch als
zählergesteuerte Schleife betrachtet. Moderner ist es sie als
Rahmen für komplexe Iterationen durch Aufzählungen zu
betrachten (wie Arrays etc.)
for ( <Anweisung bei Schleifenstart>;
<Bedingung für jeden Schleifendurchlauf>;
<Anweisung nach jedem Schleifendurchlauf> ) {
Anweisung 1;
Anweisung 2;
:
Anweisung n;
}
1.2.9 Strukturieren mit Unterprogramm- und Funktionsblöcken
Die grundlegende Blockstruktur erfordert den
Einschluss von Anweisungen in Blöcken. Mittels Unterprogramm-
und Funktionsblöcke kann der Programmierer den Quellcode
sinnvoll aufteilen, so dass Teile testbar und austauschbar werden.
Ein Unterprogrammblock ist eine benannte
Anweisungsliste. Über diesen Namen kann die Anweisungsliste
gestartet werden.
void PrintCurrentTime() {
DateTime dateTime = null;
dateTime = Now
Debug.Print ("Uhzeit " + dateTime.ToShortTimeString());
}
Aufgerufen wird es über den Namen
PrintCurrentTime();
1.2.9.1 Implementierung des EVA- Prinzips mit Unterprogrammen
Ein
Grundprinzip der Programmierung ist die Eingabe - Verarbeitung -
Ausgabe, kurz EVA.
Ein
Beispiel dafür ist die Umrechnung von Polarkoordinaten
(Drehwinkel + Abstand) in kartesische Koordinaten (x,y). Das
rotierende Flughafenradar erfasst z.B. die Position eines Flugzeugs
in Polarkoordinaten. Die umgerechnete y Koordinate entspricht der
Flughöhe.
Die Umrechnung kann als VB.NET Unterprogramm
implementiert werden. Eingaben und Ausgaben werden dabei über
eine Parameterliste abgewickelt. In dieser werden alle Ein-
und Ausgaben deklariert analog den Variablendeklarationen.
void PolarToKartesian(double r, double Phi, out double x, out double y)
| |___________________| |_________________________|
| | |
Unterprogramm- Eingabeparameter Ausgabeparameter
name
Eingabeparameter sind
vergleichbar mit lokalen Variablen des Unterprogramms, die beim
Programmaufruf mit Daten aus dem Hauptprogramm initialisiert werden:
double x, y;
PolarToKartesian(1591.2, 45.3, out x, out y) // Programmaufruf
|_______| |_________|
| |
Eingaben werden mit Daten aus Als Ausgaben werden Referenzen auf Variablen
Hauptprogramm initialisiert aus dem Hauptprogramm übergeben
Sind die Ausgabeparametern von
primitiven Typ (wie. z.B. double, Integer, …) dann müssen
sie mit dem Schlüsselwort out
gekennzeichnet
werden, damit sie als Referenzen (=Hauptspeicheradressen, Zeiger)
übergeben werden. So entspricht jede Zuweisung an einen
Ausgabeparameter im Unterprogramm einer Zuweisung der zugehörigen
Variable im Hauptprogramm.
Sind
die Ausgabeparameter Objekte (= Referenztypen), dann ist
kein
out erforderlich, da Objekte immer
als Referenzen übergeben werden.
Anbei
die vollständige implementierung der Koordinatenumrechnung:
'
Rechnet polare in kartesische Koordinaten um
void PolarToKartesian(double r, double Phi, out double x, out double y) {
double phiInRad = (Application.Pi * phi) / 180.0;
x = r * Math.Cos(phiInRad);
y = r * Math.Sin(phiInRad);
}
1.2.9.2 Selbstdefinierte Funktionen
Die Menge der vordefinierten .NET- Funktionen kann
mittels Funktionsblöcke erweitert werden.
Eingaben werden wie bei Unterprogrammen als
Parameterliste übergeben.
// Funktion, die eine römische Zahl auf einen Zahlenwert
// in arabischer Notation abbildet
long RomToArabFunc(string romNum)
|__| |___________|
| |
Typ des Argument
Funktionswertes
Im Funktionsblock wird die Abbildungsvorschrift durch eine
Anweisungsliste implementiert. Der Funktionswert wird durch Zuweisen
an den Funktionsnamen festgelegt.
// Funktion, die eine römische Zahl auf einen Zahlenwert
// in arabischer Notation abbildet
long RomToArabFunc(romNum As String) {
// Hilfsvariable anlegen, die Ergebnis der Konvertierung aufnimmt
long arabValue = 0;
// Unterprogramm zur Konvertierung von römisch in
// arabische Zahlendarstellung aufrufen
RomToArab(romNum, out arabValue);
// Funktionswert zurückgeben
return arabValue
}
1.2.9.2.1 Allgemeine Definition eines Function- Block
<Datentypname> <Funktionsname>(<Parameterliste>) {
Anweisung 1;
Anweisung 2;
:
Anweisung n;
return <Ausdruck>;
}
1.2.9.3 Call by Reference
Um Wertetypen durch einen Call by Reference zu
übergeben, gibt es das Schlüsselwort ref. Es muß
in der Parameterliste und
in beim Aufrug angegeben werden !
void mm_in_cm(float mm, ref float cm) {
cm = mm / 10F;
}
// Hauptprogramm
float cm = 0;
mm_in_cm(1000F, ref cm);
1.2.9.4 Ausgabeparameter
Die Richtung des Datenflusses kann bei Parametern
auch nur auf die Ausgabe von Werten beschränkt werden. Dies ist
ein Spezialfall des Call bey Reference. Gegenüber Call by
Reference muß die als Argument übergebene Variable im
Aufrufer nicht initialisiert sein.
void mm_in_cm(float mm, out float cm) {
cm = mm / 10F;
}
// Hauptprogramm
float cm;
mm_in_cm(1000F, out cm);
1.2.9.5 Überladen von Funktions- und Unterprogrammnamen,
Der C# Compiler unterscheidet Funktionen und
Unterprogramme nicht nur anhand ihres Namens: bei der Namensauflösung
wird auch die Parameterliste hinzugezogen, wobei die Datentypen der
Parameter und ihre Reihenfolge entscheidend sind. Diese Erweiterung
des Namensbegriffs bei Funktionen und Unterprogrammen wird auch
Signatur geannt. Hierdurch
kann ein Funktionsname mehrfach verwendet werden, man spricht von
Überladung.
class zahl
{
// 1. mul
public double mul(int a, int b)
{
return a * b;
}
// 2. mul
public double mul(float a, float b)
{
return a * b;
}
void main() {
// Hauptprogramm
zahl z = new zahl();
double x;
x = z.mul(2, 3); // 1. mul wird aufgerufen
x = z.mul(3.14F, 2F); // 2. mul wird aufgerufen
}
}
1.2.10 Datenstrukturen
Häufig ist die Abbildung von Geschäftsobjekte
auf elementare Datentypen wie z.B. int nicht ausreichend.
Vielmehr sind Kompositionen einzelner Werte zu Datensätzen
notwendig, um eine aussagekräftige Abbildung eines
Geschäftsobjekte zu erhalten.
Beispielsweise können Preise in einer
globalisierten Welt durch Datensätze aus zwei Komponenten
dargestellt werden:
Beispielpreise
|
Wert
|
Währung
|
969,71
€
|
969,71
|
EUR
|
1236,40
$
|
1236,40
|
USD
|
1172,10
SFr
|
1172,10
|
SFr
|
1.2.10.1 Deklaration
In C# können die Datensätze der
einzelnen Tabellenzeilen auf Datenstrukturen abgebildet werden. Eine
Datenstruktur wird durch einen struct -
Block deklariert:
struct <Name der Datenstruktur> {
<Komponenten == Member der Datenstruktur deklarieren> ...
}
Im Block werden die einzelnen Komponenten eines Datensatzes im
einfachsten Fall als öffentlich zugängliche Variablen
definiert. Beispiel:
enum CurrencySymbols { EUR, USD, SFr }
struct PriceDbl {
// nummerischer Anteil eines Preises
public double Value;
// Währungssymbol
public CurrencySymbols CurSym;
}
Mittels der deklarierten Datenstruktur können nun Datensätze
im Programm angelegt und verarbeitet werden:
// Preis einer Computermaus mit Default- Konstruktor anlegen
PriceDbl pMouse;
Auf die einzelnen Komponenten einer Datenstruktur kann mit dem
Memberzugriffsoperator "." zugegriffen werden:
// Zugriff auf die Member der Datenstruktur
pMouse.CurSym = CurrencySymbols.EUR;
pMouse.Value = 29.99;
1.2.10.2 Instanziierung und Konstruktoren
Das Anlegen eines neuen Datensatzes vom Typ einer
Datenstruktur wird als Konstruktion oder auch Instanziierung
bezeichnet
Wenn ein neuer PriceDbl Datensatz angelegt
wird, startet automatisch eine Initialisierungsroutine, die alle
Member auf 0 setzt. Diese Initialisierungsroutine wird auch Default-
Konstruktor genannt. Neben dem
Default- Konstruktor
können der Datenstruktur noch selbstdefinierte Konstruktoren
hinzugefügt werden, um den Aufbau der Datensätze zu
vereinfachen.
Ein
selbstdefinierter Konstruktor ist dabei ein Block, der mit dem Namen
der Datenstruktur benannt wird, und eine Parameterliste besitzt, über
die der Konstruktor beim Start konfiguriert werden kann:
struct PriceDbl
{
// nummerischer Anteil eines Preises
public double Value;
// Währungssymbol
public CurrencySymbols CurSym;
// Konstruktor (Initialisierungroutine), die das Anlegen
// eines Preises erkleichtert
public PriceDbl(double Value, CurrencySymbols CurSym)
{
// Zur Unterscheidung der Member von den gleichnamigen Parametern
// wird den Membern das Schlüsselwort this. vorangestellt
this.Value = Value;
this.CurSym = CurSym;
}
}
Nun kann ein Preis- Datensatz in einer einzigen Zeile erzeugt, und
mit den Wunschwerten initialisiert werden. Dazu wird der Konstruktor
mittels vorangestelltem Schlüsselwort new
aufgerufen, und die Wunschdaten werden in der Parameterliste
übergeben:
// Preis einer mobilen Festplatte mit Parametrierbaren Konstruktor anlegen
var pHDD = new Basics.PriceDbl(79.90, CurrencySymbols.SFr);
1.2.10.3 Geschäftslogik mittels Methoden
implementieren
Neben einer
benutzerspezifischen Initialisierungsroutine wie der Konstruktor kann
auch Geschäftslogik in der Datenstruktur direkt implementiert
werden.
public struct PriceDbl
{
// nummerischer Anteil eines Preises
public double Value;
// Währungssymbol
public CurrencySymbols CurSym;
// Liefert den Wechselkurs für den Umtausch einer Währung in USD
// vom 13.10.2014
public static double ExchangeRateToUSD(CurrencySymbols From)
{
// Kurse vom 13.10.2014
switch (From)
{
case CurrencySymbols.EUR:
return 1.275;
case CurrencySymbols.SFr:
return 1.055;
case CurrencySymbols.USD:
return 1.0;
default: throw new ArgumentOutOfRangeException(From.ToString());
}
}
// Rechnet einen Preis in USD um
public static double ToUSD(PriceDbl price)
{
return price.Value * ExchangeRateToUSD(price.CurSym);
}
// Membermetohde: hat Zugriff auf das Objekt und all seine Daten
public double ToUSD()
{
return ToUSD(this);
}
}
1.2.10.4 Merkmale von Strukturen
Strukturen sind Wertetypen
Strukturen können Eigenschaften,
Methoden und Ereignisse besitzen
Strukturen können Schnittstellen
implementieren, aber von keiner Basisklasse erben
Es können parametrisierbare
Konstruktoren definiert werden.
Der parameterlose Konstruktor new() darf
nicht überschrieben werden.
Werden Strukturvariablen ohne new
definiert, dann werden die Felder der Struktur nicht
initialisiert
Werden
Strukturvariablen mit new definiert,
dann erfolgen Standardinitialisierungen der Felder
1.2.10.5 Datum und Uhrzeit als vordefinierte Datenstruktur
Grundlage für die Datumsberechnung ist ein
linearer Zeitstrahl, der in Ticks unterteilt ist. Ein
Tick hat die Dauer von 10-7s (zehnmillionstel Sekunde).
Zeitpunkte auf dem Zeitstrahl werden durch Werte vom Typ
Date angegeben. Ein Datumswert wird hat das Format
#M/D/YYYY# (amerikanisches Datumsformat).
Date
basiert auf System.DateTime. Die Konstruktoren von
System.DateTime sind mehrfach überladen:
DateTime dat1 = new DateTime(Ticks)
DateTime dat2 = new DateTime(YYYY, MM, DD)
DateTime dat3 = new DateTime(YYYY, MM, DD, hh, min, sec, millisec)
Zur Darstellung von Zeiträumen dient die Klasse TimeSpan.
1.2.10.6 Datenstrukturen selber
definieren
Einfache,
anwednungsspezifische zusammengesetzte Datentypen wie eine x-y-
Koordinate können mittels einer selbstdefinierten Datenstruktur
implementiert werden. Grundlage dafür ist der Struktur-
Block:
namespace Basics
{
public struct SPoint
{
// Achtung: Strukturen dürfen den eingebauten Defaultkonstruktor nicht überschreiben
//public SPoint()
//{
// X = 0;
// Y = 0;
//}
/// <summary>
/// Konstrutor mit 2 Parametern.
/// </summary>
/// <param name="X"></param>
/// <param name="Y"></param>
public SPoint(double X, double Y)
{
this.X = X;
this.Y = Y;
}
public double X;
public double Y;
}
}
1.2.11 Referenztypen - Speicherverwaltung außerhalb des Stapels
Alle bis dato behandelten Datentypen wie int,
double und char haben einen zur Entwurfszeit bekannten
und festen Speicherplatzbedarf. Variablen dieser Typen können
problemlos im Stapelspeicher als globale (ganz unten im Stapel) bzw.
lokale angelegt werden. Lebensdauer, Gültigkeit und Sichtbarkeit
wird damit durch die Stapelspeicherverwaltung definiert (siehe oben).
Alle auf dem Stapel verwalteten Daten werden Wertetypen genannt.
1.2.11.1 Grenzen der Wertetypen
Es gibt aber Datentypen, deren Speicherplatzbedarf
erst zur Laufzeit ermittelt werden kann. Auch kann der
Speicherplatzbedarf einiger Datentypen zur Laufzeit variabel sein.
Und nicht immer ist die restriktive Verwaltung durch den
Stapelspeicher, welche die Lebensdauer eng an Blockgrenzen bindet,
wünschenswert.
Beispiel
für Datentypen, deren Speicherplatzbedarf erst zur Laufzeit
ermittelt werden kann, sind System.String und
System.Uri.
Einem String können
Zeichenketten beliebiger Länge zugewiesen werden. Durch String-
Operationen wie Verknüpfung oder Substring kann der
Speicherbedarf während der Laufzeit variieren.
System.Uri
ist
ein Container in dem URL's
unterschiedlichster Komplexität und Länge aufbewahrt und
analysiert werden können.
1.2.11.2 Objekt- Variablen
Objekte werden im Arbeitsspeicher anders verwaltet
als Variablen primitiven Typs. Sie werden im sog. Freispeicher
aufbewahrt, während die Variablen im Stapelspeicher residieren.
C#
kann nur direkt auf Variablen im Stapelspeicher zugreifen. Mit
folgendem Trick gelingt auch der Zugriff auf Objekte: es wird eine
Variable im Stapelspeicher angelegt und in diese die Speicheradresse
des Objekts geschrieben. Beim Zugriff auf die Variable leitet die
Laufzeitumgebung von C# dann weiter auf das Objekt.
1.2.11.3 Konstruktion mittels new Operator
Das Objekt muss anders als bei Wertetypen mittels
des new Operators explizit
angelegt werden. Erfolgt dies nicht, dann hat die Variable den
besonderen Wert null,
und alle folgenden Zugriffe auf Eigenschaften und Methoden über
diese Variable scheitern.
Uri myUri; // myUri hat den Initialwert null
Debug.Assert(myUri == null);
myUri = new Uri(); // erst jetzt wird ein Objekt angelegt, und seine Adresse
// im Heap in myUri abgelegt.
Der Zugriff auf das
Objekt erfolgt über den Variablenname. Auf Eigenschaften und
Methoden des Objektes kann mittels des . Operators zugegriffen
werden:
Debug.WriteLine(myUri.Host);
1.2.11.4 Beispiele für vordefinierte Referenztypen:
System.Object: Basisklasse aller
Datentypen in C# ("gemeinsamer Kern")
System.String: Speichern von
Zeichenketten variabler Länge. Sonderfall: Compiler und
Laufzeitumgebung übernehmen die Instanziierung mit new
wg. Implementierung besonders effizienter Speicherplatzverwaltung.
Beim Instanziieren darf der Programmierer nur mit Zeichenketten
initialisieren wie
string txt = "Hallo Welt"; // und nicht new String("Hallo Welt");
System.Random:
Zufallszahlengeneratoren. Die einzelnen Objekte erzeugen voneinander
unabhängige Zufallszahlenreihen
var rnd1 = new Random(1);
var rnd2 = new Random(2);
// Zufallszahlen im Bereich zw. 1 und 100 mittels zweier
// unabhängiger Zufallszahlengeneratoren erzeugen
int num = rnd1.Next(1, 100);
num = rnd2.Next(1, 100);
1.2.11.5 Referenztypen mittels Klassen selber definieren
1.2.11.6 Wertetypen als Referenztypen: Boxing und
Unboxing- Konvertierung
Die strenge, in der CLS vorgeschriebene
Objektorientierung von C#, bedingt, dass auch Wertetypen sich in
bestimmten Situationen wie Objekte verhalten müssen. Soll z.B.
ein doppelt genauer Gleitkommawert in einen String gewandelt werden,
dann gelingt dies mit folgender Anweisung:
string txtVal = (3.142).ToString();
Eine double- Konstante ist ein Wertetyp, und hier wird für diese
eine Methode aufgerufen !
Diese verblüffende Merkmal ermöglicht in
C# der sogenannt boxing-unboxing Mechanismus:
Wird ein Wertetyp in einem Objektkontext aufgerufen, dann wird
temporär auf dem Heap ein Objekt angelegt, und der Wert des
Wertetypen hineinkopiert. Dies wird als boxing bezeichnet.
// zeichen ist ein Wertetyp und
char zeichen = 'A';
// Im folgenden wird der Wertetyp in einem Objektkontext eingesetzt
// Dabie wird auf dem Heap ein Objekt erzeugt, das eine Kopie des Wertes
// von zeichen aufnimmt.
string aussage = "";
aussage = zeichen.isDigit() ? "Ziffer" : "keine Ziffer";
Aus einer Objektbox kann der ursprüngliche Wertetyp mittels
eines Typecast (Konvertierung) wiederhergestellt werden. Dieser
Prozess wird als unboxing bezeichnet.
// Wird ein Wertetyp als Objekt- Referenz einem unterprogramm übergeben,
// dann ist er in eine Objektbox verpackt. Aus dieser muss er mittels eines
// Konvertierungsoperators wieder ausgepackt werden
void TueWas(object oZeichen) {
// Unboxing- Konvertierung
char zeichen = (char)(oZeichen)
...
}
Wichtig beim unboxing ist, das aus dem object- Typ in den
ursprünglichen Wertetyp, und nicht in einen verwandten. Sonst
scheitert das unboxing während der Laufzeit mit einer
Invalid Cast Exception:
public static double AddUntypisiert(object a, object b)
{
// Achtung: a und b werden als Objektboxen übergeben.
// Wenn b ein integer war, und nun als integerobjektbox übergeben wurde,
// dann ist dafür kein (double) Konvertierungsop. definiert
// => Unboxing durchführen in
// Ohne Downcast läuft nichts
// return a + b;
//return (double)a + (double)b;
return Unboxing(a) + Unboxing(b);
}
public static double Unboxing(object box)
{
// Kann man auch mit Convert.ToDouble(box) realisieren
if (box is int)
return (double)(int)box;
// gleich in Double wandeln geht nicht !!
//return (double)box;
else if (box is short)
return (double)(short)box;
if (box is double)
return (double)box;
else
throw new InvalidCastException();
}
1.2.12 Tupel und generische Typen
Im Folgenden soll anhand des speziellen Typs Tuple
das Funktionsprinzip generischer
Typen eingeführt werden.
Generische Typen
gibt es seit .NET 2.0. Konzeptionell sind sie den
Templates
der Sprache C++ ähnlich,
die Vorlage für die Entwicklung generischer Typen in C# waren.
Durch generische Typen
können Datenstrukturen und Algorithmen auf einer sehr hohen
Abstraktionsstufe beschrieben werden. Aus dem Abstrakten kann der
Programmierer eine Vielfalt von speziellen Typen und Algorithmen sehr
einfach erzeugen. Dadurch sind mit kleinen, kompakten
Klassenbibliotheken mannigfaltige Aufgaben lösbar. Generische
Typen implementieren hochgradig wiederverwendbaren Code in C#.
1.2.12.1 Tupel
Viele Geschäftsprozesse lassen sich als
Mengen im Computer abbilden. Die Elemente solcher Mengen werden in
der Mathematik allgemein durch Tupel dargestellt.
Ein Tupel ist ein
Verbund einzelner Elemente aus verschiedenen Mengen. Sie entstehen
durch Kreuzprodukte zwischen Mengen.
Wenn z.B. dem
Autokäufer drei Grundfarben und zwei Ausstattungsvarianten zur
Verfügung stehen, dann kann dies Mengentheoretisch wie folgt
dargestellt werden:
Farben := {rot, grün, blau}
Varianten := {basis, lux}
Optionen als Kreuzprodukt:
Optionen := Farben x Varianten := { (rot, basis), (grün, basis),
(blau, basis),
(rot, lux), (grün, lux), (blau, lux)}
Paare wie (rot, lux)
werden als Tupel bezeichnet. Tupel
können aus 1-n Komponenten bestehen, abhängig davon, wie
viele Mengen am Kreuzprodukt teilnahmen.
1.2.12.2 Der generische
Typ System.Tuple<...>
C# ermöglicht die
Darstellung durch den generischen Typ Tupel:
System.Tuple<T1, T2, …> {
// Erste Komponente als nur lesbare Eigenschaft
public T1 Item1{ get; }
// Zweite Komponente als nur lesbare Eigenschaft
public T2 Item2{ get; }
...
}
T1, T2, … sind dabei sog. Typ- Parameter, welche den Datentyp
der jeweils einzelnen Member der Klasse (hier die Komponenten Item1,
Item2, … eines Tupels) definieren. Die in spitzen Klammern
<...> gesetzte Liste von Typ- Parametern unmittelbar hinter dem
Klassennamen wird als Typ- Parameterliste bezeichnet.
Benötigt man z.B. Tupel, für die
Kombinationen aus Farben und Ausstattungsvariante von Fahrzeugen,
dann kann dessen Datentyp aus System.Tupel<T1, T2, …
> wie folgt erzeugt werden:
enum Farben { rot, grün, blau}
// Ergebnis, wenn Typen an Typ- Parameterliste übergeben wurden, und
// Compiler eine Klasse aus dem generischen Typ erzeugt hatte:
System.Tuple<Farben, string>{
// Erste Komponente als nur lesbare Farben- Eigenschaft für die Farbe
public Farben Item1{ get; }
// Zweite Komponente als nur lesbare string- Eigenschaft für die Ausstattung
public string Item2{ get; }
...
}
Den Typparametern T1 und T2 werden
die Klassen Farben und
string übergeben.
Dabei instanziiert der Compiler eine Klasse aus dem generischen Typ,
wobei die Komponenten die gewünschten Typen haben.
var t1 = new Tuple<Farben, string>(Farben.rot, "basis");
var t2 = new Tuple<Farben, string>(Farben.grün, "basis");
…
1.2.13 Nullable Typen (NET 2.0)
Die Datentypen, die in Datenbanken verwendet
werden besitzen gegenüber den Datentypen einer
Programmiersprache wie C# einen besonderen Zustand: den Null- Wert.
Dieser tritt auf, wenn eine Tabellenzeile angelegt, aber nicht allen
Spalten gleichzeitig ein Wert zugewiesen wurde.
Um die Programmierung von Datenbanken mit C# zu
erleichtern wird ab .NET 2.0 ein allgemeines Framework zum Umgang mit
solchen Null- Typen bereitgestellt. Kern ist hierbei die generische
Struktur System.Nullable<T>:
C# vereinfacht den Umgang mit Nullable- Typen,
indem es für Sie eine Spezielle Syntax anbietet:
// Deklaration eines Nullable Typs
Nullable<int> x;
// Deklaration in vereinfachter C#- Syntax mittels ? Postfix
int? y;
// Auslesen eines Wertes aus einem Nullable Typen, wobei im Falle eines null- Wertes
// ein Standardwert ausgegeben wird
int u = x.GetValueOrDefault(99);
// Der gleiche Vorgang in vereinfachter C# Syntax
int v = x ?? 99;
// null zuweisen, um anzuzeigen, dass die Eigenschaft/der Wert nicht existieren
x = null;
u = x.GetValueOrDefault(99);
…
void queryMethod1(out int? CountRows) {
SqlCommand cmd("select count(*) from tab1", "…");
CountRows = cmd.ExecuteNonQuery();
}
…
int? CountR;
queryMethod1(out CountR);
1.2.13.1.1 Primzahlscanner imperativ implementieren
Teilaufgabe 1 wird durch folgende Befehlssequenz
gelöst (Pseudokode):
Primzahltest(In: Prüfling, Out: IstPrimzahl)
Dim Teiler = 2
Wiederhole bis Teiler = Prüfling / 2 + 1
Wenn Prüfling Mod Teiler = 0 dann
IstPrimzahl := false
Goto Ende Primzahltest
Sonst
Teiler := Teiler + 1
Ende Wenn, dann, sonst
Ende Wiederholung
IstPrimzahl = t
Ende Primzahltest
Teilaufgabe 2 implementiert folgende Befehlssequenz
Primzahlsuche(In: a, In: b, Out: ListePrimzahlen)
Dim Prüfling = a
Wiederhole bis Prüfling = b + 1
Dim IstPrimzahl
Primzahltest(In: Prüfling, Out: IstPrimzahl)
Wenn IstPrimzahl dann
ListePrimzahlen erweitern um Prüfling
Ende Wenn, dann
Prüfling = Prüfling + 1
Ende Wiederholung
Ende Primzahlsuche
1.2.14 Arrays
Zur Verarbeitung
tabellarischer Daten dienen in C# Arrays. Durch die Deklaration eines
Arrays werden im RAM n Speicherplätze zur Aufnahme von Werten
des Typs x reserviert.
Arrays sind
Referenztypen.
Alle Arrays sind von
der Klasse System.Array abgeleitet. Diese dient C# intern als
Basisklasse zur implementierung der Arrays. Von System.Array kann
nicht abgeleitet werden !
1.2.14.1 Deklaration
Deklaration eines
Arrays hat folgenden Aufbau:
int[] a = new int[4];
| | +------------- Anzahl der Einträge
| +--------------- Name des Arrays
+------------------ Typ der Einträge (Array)
double[,] tabWegZeit = new int[2, 10]; // Zweidimensionales Array
Array sind Referenztypen. Nach einer Arraydeklaration ergibt sich
folgendes Format im Arbeitspeicher:
1.2.14.2 Zugriff
Der Zugriff auf die
Elemente eines Arrays erfolgt über den Namen und den Indize:
int x;
// x hat nach Ausführung dieser Anweisung den Wert 2
x = a[2]
| +--- Indize (Platznummer)
+----- Arrayname
// Dokumentation der Bewegung eines Körpers von 500m in 300 s in einem 2D- Array
tabWegZeit[0, 1] = 300; // 300 s
tabWegZeit[1, 1] = 500; // 500 m
Die Indizes können dynamisch zur Laufzeit festgelegt werden.
Damit ist eine komfortable Verarbeitung der Arraydaten in Schleifen
möglich:
int i;
long s;
// s enthält nach der Schleife die Summe aller Einträge
for (int i = 0; i < a.length; i++)
s = s + a[i];
1.2.14.3 Initialisierung von Arrays
int[] primz = {2, 3, 5, 7, 11};
1.2.14.4 Anzahl der Einträge bestimmen
int anz_elems = tabWegZeit.length; // Anzahl der Elemente in über alle Dimensionen
int anz_dims = tabWegZeit.rank; // Anzahl der Dimensionen eines Arrays
int anz_elem_in_dim_1 = tabWegZeit.GetLength(1) // Anzahl der Elemente in Dimension 1
1.2.14.5 Zuweisen und Kopieren
Durch Zuweisung wird nur eine Referenz erzeugt,
jedoch keine Kopie.
int[] primz2 = primz;
Die Methode Clone() eines Arrays erzeugt eine 1:1 Kopie
int[] primz3 = (int[])primz.Clone();
1.2.14.6 Besuchen aller Einträge eines Arrays mittels foreach -
Schleifenblock
Ein Spezialfall der
for- Schleife ist eine Iteration durch eine Aufzählung, bei der
jedes Element der Aufzählung besucht wird. Dieser häufige
Spezialfall wird in c# durch eine foreach Schleife formuliert:
int[] primz= {3, 5, 7, 11, 13, 17};
foreach int p in primz {
Console.WriteLine(p.ToString() + '\n');
}
1.2.14.7 Parameterarrays
Mittels des Schlüsselwortes params kann
eine Funktion eine Parameterliste mit beliebig vielen Elementen
erhalten:
public long summe_aus(params int[] liste) {
long summe;
foreach(long x in liste)
summe += x;
}
return summe;
}
// Aufruf
long sum2 = summe_aus(1, 2);
long sum5 = summe_aus(1, 2, 3, 4, 5)
1.2.14.8 Arrays sortieren
string[] planeten = {"Merkur", "Venus", "Erde", "Mars", "Jupiter", "Saturn"};
Array.Sort(planeten);
1.2.14.9 Array mit selbstdefinierten Typen Sortieren
enum ELaenge {mm, cm, dm, m, km};
struct Laenge : IComparer
{
public float wert;
public enumLaengenEinheiten einheit;
public int Compare(object obj1, object obj2)
{
Laenge l1 = (Laenge)obj1;
Laenge l2 = (Laenge)obj2;
// alles in mm umrechnen
float il1 = tomm(l1), il2 = tomm(l2);
if (il1 == il2)
return 0;
else if (il1 < il2)
return -1;
else
return 1;
}
float tomm(Laenge l)
{
switch (l.einheit) {
case ELaenge.mm:
return l.wert;
case ELaenge.cm:
return l.wert * 10;
case ELaenge.dm:
return l.wert * 100;
case ELaenge.m:
return l.wert * 1000;
case ELaenge.km:
return l.wert * 1000000;
}
return -1;
}
}
// In Main
Laenge[] fahrten = new Laenge[3];
fahrten[0].einheit = ELaenge.mm;
fahrten[0].wert = 100000;
fahrten[1].einheit = ELaenge.cm;
fahrten[1].wert = 3000;
fahrten[2].einheit = ELaenge.km;
fahrten[2].wert = 0.99F;
Array.Sort(fahrten, fahrten[0]);
foreach (Laenge fahrt in fahrten)
{
Console.WriteLine("{0} {1}", fahrt.wert, fahrt.einheit.ToString());
}
1.2.14.10 Aufg.:
Min/Max- Suche,
Sortieren
Messwerterfassung (akt. Wert, dyn.
Mittelwert)
1.2.15 char
1.2.15.1 Objektmodell von char
1.2.15.2 Char
, Strings und Unicode
.NET unterstützt Unicode. Quelltexte können
als Unicode- Dateien verpackt werden, und Zeichentypen wie char
und string basieren auf 16bit Unicode. Folgendes Windows-
Programm liefert einen Dialog mit einem kyrillischen Text:
public class Form1 : System.Windows.Forms.Form {
#Region " Vom Windows Form Designer generierter Code "
:
#End Region
private Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs)
// char verarbeiten Unicode- Zeichen
char c1, c2, c3, c4, c5, c6, c7;
c1 = '\u041B';
c2 = '\u042E';
c3 = '\x0411';
c4 = '\x041E';
c5 = '\x0412';
c6 = '\x042C';
string str;
str += c1.ToString() + c2.ToString() + c3.ToString() + c4.ToString() + c5.ToString() + c6.ToString();
lblDasWichtigste.Text = str;
}
}
1.2.16 Strings
Siehe .NET Doku
1.2.16.1 Teilstrings ausschneiden
string txt = "Anton, Berta, Cäsar";
Console.WriteLine(txt.Substring(8, 5));
1.2.16.2 Umwandeln in Groß- oder Kleinschreibung
string txt = "Hallo Welt";
Console.WriteLine("Alles groß: {0}", txt.ToUpper);
Console.WriteLine("Alles klein: {0}", txt.ToLower);
1.2.16.3 Startposition von Teilstrings bestimmen
string txt = "Anton, Berta, Cäsar";
int pos = txt.IndexOf("Berta");
1.2.16.4 Einfügen in Strings
string txt = "Anton, Berta, Cäsar";
txt = txt.Insert(txt.IndexOf("Berta"), "Aladin, ");
1.2.16.5 Auftrennen von Zeichenfolgen
string txt = "Anton,Berta,Cäsar";
string[] namen = txt.Split(',');
1.2.16.6 Testen mittels regulärer Ausdrücke
Das folgende Prädikat testet, ob ein Ausdruck
dem Muster eines GUID entspricht
public static bool IsGuid(string expr)
{
if (expr != null)
{
Regex guidRegEx = new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$");
return guidRegEx.IsMatch(expr);
}
return false;
}
|
1.2.16.7 Splitten mittels Reguläre Ausdrücke
System.Text.Regex r = new Regex("\\s*=\\s*");
string [] aw = r.Split("Durchmesser = 199");
Console.WriteLine ("Attribut {0} hat Wert {1}", aw[0], aw[1]);
1.2.16.8 Ersetzen mittels Regulärer Ausdrücke
// In folgendem String sollen alle Vorkommen von keinen durch [] markiert werden
String txt = "Das Pferd frisst keinen Gurkensalat. Ich habe keine Ahnung warum";
Regex r = new Regex();
string txt_neu = r.Replace(txt, keinen, "[" + keinen + "]")
1.2.16.9 Implentierungsdetails von Strings
Strings werden in .NET trickreich verwaltet, um
den Resourcenbedarf zu minimieren. Dabei werden die String- Werte in
einer Hashtable abgelegt. Wenn mehrere Stringvariablen den gleichen
Wert haben, dann teilen sie sich alle denselben Eintrag in der
Hashtable. Änderungen am Inhalt einzelner Stringvariablen werden
durch Anlegen neuer Einträge in der Hashtable mit den
Ergebnissen der Manipulation realisiert. Aus diesem Grunde ist eine
direkte Manipulation der Stringinhalte nicht möglich- alle
Stringfunktionen lassen das Orginal unberührt und liefern einen
neuen String mit dem Ergebnis der Manipulation
// Strings sind Referenztypen
string s1 = "Hello World";
// Kopieren von Referenzen: s1 und s2 zeigen auf das gleiche Objekt
string s2 = s1;
Trace.WriteLineIf(object.ReferenceEquals(s1, s2), "&s1 == &s2");
// Besondere Speicherverwaltung für Strings: anstatt mehrere Kopien
// des gleichen Strings wird intern in einer Hashtable auf den gleichen
// Eintrag verwiesen
string s3 = "Hello World";
Trace.WriteLineIf(object.ReferenceEquals(s1, s3), "&s1 == &s3");
// Werden Strings verändert, dann legt die Laufzeit für den geänderten einen
// neuen Eintrag in der Hashtable an
s3 += "!";
Trace.WriteLineIf(object.ReferenceEquals(s1, s3), "&s1 == &s3 + \"!\"");
// Stringbuilders scheinen immer einen neuen Eintrag in der Hashtable
// anzulegen, unabhängig davon, ob der Wert schon existiert
System.Text.StringBuilder bld = new StringBuilder(10);
bld.Append("Hello ");
bld.Append("World");
string s4 = bld.ToString();
Trace.WriteLineIf(object.ReferenceEquals(s1, s4), "&s1 == &s4");
// Gegenprobe: Neue Strings mit "alten Werten" verweisen wieder auf den
// alten Eintrag in der Hashtabelle
string s5 = "Hello World";
Trace.WriteLineIf(object.ReferenceEquals(s1, s5), "&s1 == &s5");
1.2.16.10 Quellen
Bart
de Smet: .NET 2.0 string interning inside out
Wesner
Moise: strings UNDOCUMENTED
1.2.16.11 Aufgaben
Romzahlkonverter
1.3 Erweiterte Grundlagen
1.3.1 Typinformationen zur Laufzeit und Reflektion
1.3.1.1 Typ bestimmen mittel is Operator
Mittels des is Operators mit kann zu einem
Objekt der Typ bestimmt werden wie folgt:
if (x is int) {
....
}
1.3.1.2 Metainformationen zu einer Instanz
Jedes Objekt erbt von der .net Wuzel System.Object
die Methode GetType(). Diese Liefert eine Referenz auf ein
Type- Objekt, welches alle Metainformationen zum Objekt enthält.
1.3.1.3 Metainformationen zu einer Klasse mittels typeof
– Operator
Die Klassenmethode System.Type.GetType(object
Classname) bestimmt für einen Typ (Klassenname) das Type-
Objekt, ohne daß ein Instanz notwendig ist.
Mittels des typeof- Operators kann zu einer
Klasse ein Typ- Objekt erzeugt werden. Er entspricht der
statischen Methode System.Type.GetType(..).
1.3.1.4 Reflektion
Die in den Type- Objekten gespeicherten
Metainformationen beschreiben die Struktur einer Klasse vollständig.
Beispielsweise können alle Member einer Klasse wie folgt
aufgelistet werden:
using System;
using System.Reflection;
class CTest {
int ganzZahl;
enum EPartei {gut, boese, gerecht};
int TueWas() {
ganzZahl ++;
return ganzZahl;
}
}
private static void PrintMetadata(Type typeObj)
{
Debug.WriteLine("Typ: " + typeObj.FullName);
Debug.WriteLine("-definiert in Assembly: " + typeObj.AssemblyQualifiedName);
Debug.WriteLine("-Merkmale:");
if (typeObj.IsClass)
Debug.WriteLine(" + ist eine Klasse");
if (typeObj.IsEnum)
Debug.WriteLine(" + ist ein Enum");
if (typeObj.IsGenericType)
Debug.WriteLine(" + ist generic");
if (typeObj.IsPrimitive)
Debug.WriteLine(" + ist elementarer Datentyp");
if (typeObj.IsPublic)
Debug.WriteLine(" + ist öffentlich");
if (typeObj.IsSerializable)
Debug.WriteLine(" + ist serialisierbar");
if (typeObj.IsValueType)
Debug.WriteLine(" + ist Wertetyp");
if (typeObj.IsArray)
Debug.WriteLine(" + ist ein Array");
// Liste aller Schnittstellen
Debug.WriteLine("-Schnittstellen:");
foreach (Type iFace in typeObj.GetInterfaces())
Debug.WriteLine(" + hat Schnittstelle " + iFace.FullName);
// Liste aller Eigenschaften
Debug.WriteLine("-Eigenschaften:");
foreach (System.Reflection.PropertyInfo pi in typeObj.GetProperties())
Debug.WriteLine(" + hat Eigenschaft " + pi.Name);
// Liste aller Methoden
Debug.WriteLine("-Methoden:");
foreach (System.Reflection.MethodInfo mi in typeObj.GetMethods())
{
Debug.Write(" + hat Methoden "
+ mi.ReturnType.Name + " "
+ mi.Name + "(");
foreach (System.Reflection.ParameterInfo param in mi.GetParameters())
Debug.Write(param.ParameterType.Name + " " + param.Name + ", ");
Debug.WriteLine(")");
}
}
:
void main() {
// Typinfo abfragen (Reflektion)
Type t = typeof(CTest);
PrintMetadata(typeof(CTest));
PrintMetadata(typeof(DMS.FC.ContentVector));
PrintMetadata(typeof(DMS.JobProgressInfo));
PrintMetadata(typeof(DMS.Job.JobPriorities));
}
1.3.1.5 Aufgaben
Entwickeln Sie eine Klasse CTypeBrowser,
welche die Methode info(object) implementiert, die zu einem
gegebenen Objekt alle Felder und Methoden auflistet.
1.3.2 Elementare Ein/Ausgabe
Beispiel:
decimal preis = 99.45;
// Preis wird als Währungswert formatiert
Console.WriteLine("Preis: " + preis.ToString("C"));
1.3.2.1 Formatzeichenfolgen
Über Formatzeichenfolgen wird abhängig
vom Datentyp die Darstellung eines Wertes als String gesteuert. Es
gibt Formatzeichenfolgen für:
nummersiche Typen
Datumstypen
Aufzählungstypen
1.3.2.2 Formatzeichenfolgen für nummerische Typen
Formatzeichenfolgen bestehen aus einem
Formatbezeichner und optional aus der Angabe einer
Genauigkeit XX. Um eine Zahl mit max. 3 Nachkommastellen
auszugeben, kann folgende Formatzeichenfolge verwendet werden:
N3
Folgende Formatbezeichner sind vordefiniert:
Formatbezeichner
|
Bedeutung
|
Beispiel
|
Resultat
|
C
|
Währungsbetrag
|
(99.490).ToString("C");
|
99,49 €
|
D
|
Ganzzahliger Wert. Die Genauigkeit definiert
die minimale Ausgabebreite. Sollte die darzustellende Zahl die
minimale Ausgabebreite nicht erreichen, dann wird mit führenden
0-en aufgefüllt
|
(123).ToString("D5");
|
00123
|
E
|
Zahl in Exponentialschreibweise
|
(99.49).ToString("E");
|
9,95E+005
|
F
|
Festkomma. Die Genauigkeit gibt eine feste
Anzahl von Nachkommastellen an
|
(99.49).ToString("F1");
|
99,5
|
G
|
Allgemein- Formatierung erfolgt abhängig
vom Typ der Zahl
|
(99.49).ToString("G");
|
99,5
|
N
|
Zahl (allgemein). Die Genauigkeit beschreibt
die Anzahl der Nachkommastellen
|
(99.49).ToString("N");
|
99,49
|
P
|
Prozentzahl. Der Wert wird mit 100
multiplizieret und mit einem Prozentzeichen versehen
|
(0.15).ToString("P");
|
15,00%
|
H
|
Hexadezimal
|
(0xA1).ToString("H");
|
A1
|
R
|
Stellt Zahlen so als Zeichenkette dar, daß
sie aus diesen ohne Genauigkeitsverlust wieder zurückkonvertiert
werden können.
|
(99.49).ToString("R");
|
99,49
|
Neben vordefinierten Formatzeichenfolgen können
Formate wie im obigen Beispiel auch selbst definiert werden. Siehe
dazu: Benutzerdefinierte
Formatzeichenfolgen
1.3.2.3 Formatzeichenfolgen für Datumstypen
Formatbezeichner
|
Bedeutung
|
Beispiel
|
Resultat
|
d
|
Kurzes Datumsformat
|
DateTime.Now.ToString("d");
|
27.04.06
|
D
|
Langes Datumsformat
|
DateTime.Now.ToString("D");
|
27. April 2006
|
T
|
Zeitformat
|
DateTime.Now.ToString("T");
|
12:17:59
|
s
|
ISO 8601 konformes Datumsformat. Werte sind
sortierbar. Dieser Typ wird z.B. in XML- Schema angewendet
|
DateTime.Now.ToString("s");
|
2006-04-27T12:17:47
|
1.3.2.4 Formatzeichenfolgen für Aufzählungstypen
Formatbezeichner
|
Bedeutung
|
Beispiel
|
Resultat
|
G
|
Stellt Wert eines Aufzählungstyps als
String dar, falls möglich. Andernfalls wird der Wert als
Integer- Zahlenwert dargestellt
|
(SUnits.cm).ToString();
|
"SUnits.cm"
|
F
|
Stellt Wert eines Aufzählungstyps als
String dar, falls möglich. Andernfalls wird der Wert als
Integer- Zahlenwert dargestellt
|
(SUnits.cm).ToString();
|
|
D
|
Stellt Wert eines Aufzälungstyps als
dezimalen Integer dar
|
(SUnits.cm).ToString();
|
2
|
X
|
Stellt Wert eines Aufzälungstyps als
hexadezimalen Integer dar
|
(SUnits.cm).ToString();
|
0x2
|
1.3.2.5 ToString und Formatierung
1.3.2.6 Ausgabe
mit IFormatProvidern
using System.Globalization;
// Kopie des aktuell wirksamen Formatproviders erzeugen
NumberFormatInfo nif= (NumberFormatInfo)System.Threading.Thread.CurrentThread.CurrentCulture.NumberFormat.Clone();
// Währungssysmbol auf Dollar umstellen
nif.CurrencySymbol = "$";
// Ausgabe eines Währungswertes
double preis = 99.99;
Console.WriteLine(preis.ToString("C", nif));
1.3.2.7 Composit- Formating
In Methoden wie Console.WriteLine("Formatstring",
object, ...) können Textkonstanten und Formatblöcke,
die Formatzeichenfolgen für auszugebende Parameter enthalten,
kombiniert werden. Dies wird als Composit- Formating bezeichnet. Soll
z.B. ein Längenmaß ausgegeben werden, welches aus dem Wert
und der Längeneinheit besteht, dann könnte dies durch
folgende Anweisung erfolgen:
Console.WriteLine("Längenmaß: {0,5:N} {1,2}", LMasz.value, LMasz.unitToString());
Ein Formatblock hat folgende Syntax:
{ParamListenIndex[,alignment][:Formatzeichenfolge]}
Der ParamListenIndex bezeichnet den Parameter aus der
Parameterliste, dessen Wert durch den Parameterblock formatiert
werden soll. Das Alignment definiert die Ausrichtung und
mindestbreite. Positive Werte stehen für eine Rechtsausrichtung,
negative für eine Linksausrichtung.