Inhaltsverzeichnis         

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.

  1. Erste Zeichen muss ein Buchstabe sein:

    Richtig: A1

    Falsch: 1A

  2. Der Name darf keine Leerraumzeichen enthalten

    Richtig: neuerMitarbeiter

    Falsch: neuer Mitarbeiter

  3. 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:

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

  2. Es gibt einen Eintrag, der den unauflösbaren Namen als Alias für einen Namensraum definiert

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

Operatorkategorie

Operatoren

Arithmetisch

+   -   *   /   %

Logisch (boolesch und bitweise)

&   |   ^   !   ~   &&   ||   true   false

Zeichenfolgenverkettung

+

Inkrementieren, Dekrementieren

++   --

Verschieben

<<   >>

Relational

==   !=   <   >   <=   >=

Zuweisung

=   +=   -=   *=   /=   %=   &=   |=   ^=   <<=   >>=

Memberzugriff

.

Indizierung

[]

Typumwandlung

()

Bedingt

?:

Delegatverkettung und -entfernung

+   -

Objekterstellung

new

Typinformationen

as   is   sizeof   typeof   

Überlaufausnahmesteuerung

checked   unchecked

Dereferenzierung und Adresse

*   ->   []   &

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

  • Wertetyp

  • decimal kann nur explizit in einen double oder float konvertiert werden

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:

  1. wiederverwendbar gemacht werden (Unterprogramme)

  2. Wiederholt ausgeführt werden (Schleife)

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

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:

  1. System.Object: Basisklasse aller Datentypen in C# ("gemeinsamer Kern")

  2. 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");
  3. 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.:

  1. Min/Max- Suche,

  2. Sortieren

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

  1. Bart de Smet: .NET 2.0 string interning inside out

  2. Wesner Moise: strings UNDOCUMENTED

1.2.16.11 Aufgaben

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

  1. 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:

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.