Reflections in C#

Es kann zur Laufzeit eines Programms notwendig sein, dass das Programm sich selbst analysieren und ggf. verändern kann. In C# kann dies mir der Funktionalität von Reflections erreicht werden.

Reflection „reflektiert“ zur Laufzeit aufrufbare Assemblys, Module und Typen und bietet so dynamischen Zugriff auf Objekte, Methoden und Metadaten, welche erst zur Laufzeit bekannt werden.
Eine Programmiersprache, welche diese Möglichkeit zur „Reflektion“ der eigenen Programminhalte hat, kann ihre eigene Struktur selbst zur Laufzeit analysieren und auf sie Einfluss nehmen.

Reflection als Methode zum dynamischen Aufruf von Klassen, Methoden, Variablen, Ressourcen usw. ist abgesehen von C# (und anderen .NET-Programmiersprachen) auch in Java bekannt und in noch einigen weiteren Programmiersprachen in ähnlicher Form verbreitet. Aber auch die Reflection-Funktionalität von Java und C# sind nicht absolut identisch. Während Java auf Klassenebene „reflektiert“ und generell auch Klassen als Informationsempfänger verwendet, kommt C# nicht über Klassen, sondern über Assemblies an die gewünschten Informationen heran.

Grundsätzlich sollte der Einsatz von Reflections vermieden werden, denn der dynamische Aufruf ist wesentlich langsamer, als der direkte Aufruf. Insgesamt ist der Einsatz dieser Funktionalität mit übermäßigen Performance-Einbußen verbunden. Mit der Anwendung von Reflections erkauft sich der Programmierer Flexibilität zur Laufzeit, nimmt damit aber Leistungseinbußen in Kauf.
Nur wenn ein dynamischer Aufruf unbedingt notwendig ist, gibt es für Reflections grundsätzlich keine Alternative. Beispiele für Anwendungsgebiete sind Schnittstellen für Plugins, der Debugger und auch die automatische Textvervollständigung in Entwicklungsumgebungen (z. B. Microsofts IntelliSense).

→ WEITERLESEN

Überladen von Methoden

Im Laufe eines Software-Projekts sind Algorithmen zu entwerfen und in Methoden zu schachteln, welche zwar prinzipiell das gleiche tun, aber mit Werten von unterschiedlichen Datentypen oder einer verschiedenen Anzahl an Werten. Für solche Fälle bietet C# die Möglichkeit zur Überladung von Methoden.

Die meisten höheren Programmiersprachen erlauben überladene Methoden, neben C# beispielsweise auch C++ und Java.
Überladene Methoden haben zwei oder mehr Signaturen. Die Signatur ist die Kopfzeile einer Methode, welche die Zugriffsmöglichkeit, den Rückgabewert, den Methodennamen und die Parameter (Namen, Anzahl und Typ) beschreibt.
Die Überladung bezieht sich in C# (und den meisten anderen Programmiersprachen auch) jedoch ausschließlich auf die Parameteranzahl und die Datentypen der Parameter, denn nur diese sind bei einem Methodenaufruf maßgebend für eine klare Abgrenzung der verschiedenen Ausführungen einer überladenen Methode.

→ WEITERLESEN

Kategorien C#

typeof() vs .GetType()

Um den Typen nach System.Type bestimmen zu können, werden entweder der Operator typeof() oder die Instanzmethode getType() verwendet. Diese beiden Möglichkeiten liefern ein Objekt vom Typ System.Type zurück.

Oft wird die Frage gestellt, welcher Unterschied zwischen diesen beiden Möglichkeiten der Typ-Bestimmung besteht.

Der typeof()-Operator kann nur das Objekt von System.Type eines Typen (eine Klasse) bestimmen.

(Achtung: In C# sind auch die oft als primitiven Typen bezeichneten Wertetypen, z. B. Integer (int) oder Double (double) Klassen!)

Dabei kann der typeof()-Operator auch generische Typen bestimmen. Überladen werden, kann dieser Operator jedoch nicht.

[csharp] System.Type type = typeof(double);
[/csharp]

getType() ist eine Methode einer Instanz (eines Objekts). getType() kann daher nicht frei verwendet werden, wie der typeof()-Operator, sondern ist an eine Instanz gebunden. getType() liefert den Typen, der eigenen Instanz zurück.

[csharp] double d = 5.4;
System.Type type = d.GetType();
[/csharp]

getType() kann im Gegensatz zu typeof() auch überladen werden. GetType() kann allerdings nicht (typeof() natürlich auch nicht) überschrieben werden.

Auf den Punkt gebracht: Zur Bestimmung von Typen von Klassen wird der Operator typeof() benutzt. Die Bestimmung von Typen von Objekten ist hingegen die Methode getType() zu benutzen.

→ WEITERLESEN

Ausnahme-Behandlung in C# (Exception-Handling)

Ein Programm, welches mit einer höheren Programmiersprache entwickelt wurde, interagiert mit seiner Umwelt, neben anderen Programmen vor allem mit dem Betriebssystem, über dieses auch mit der Hardware.

Während das Programm bis in das kleinste Detail durchdacht und nach unzähligen Tests auch (scheinbar) völlig fehlerfrei funktioniert, kann es bei der Interaktion zwischen dem Programm mit seiner Umwelt zu Problemen kommen.

Beispielsweise könnte es beim Öffnen einer Datei zu einem Fehler kommen, die Datei lässt sich nicht öffnen (obwohl das Programm vorher zumindest die Existenz der Datei festgestellt hatte). Möglicherweise lässt sich eine Datei auch nicht speichern, da der Speicherplatz nicht ausreicht, das Speichermedium entfernt wurde oder defekt ist. Nur einige von sehr vielen Beispielen von sogenannten Exceptions.
Einige Exceptions lassen sich bis zu einem bestimmten Grad vorbeugen; Beispielsweise lässt sich unmittelbar vor der Beanspruchung von Speicherplatz prüfen, ob der Speicherplatz ausreicht. Ob der Speicherplatz dann aber bei der Speicherung tatsächlich vorhanden ist und nicht schon von einem anderen Vorgang belegt wurde, ist erst bei der Speicherung feststellbar.

Mit einem bestimmten System lassen sich solche Fehler zwar nicht verhindern, jedoch behandeln.
Software-Entwickler sprechen hierbei von Fehlerbehandlung oder (besser:) Ausnahmebehandlungen, also die Behandlung von Ausnahmen (Exceptions).

Mit einer Ausnahmebehandlung, wie sie z.B. in C++, Java und C# möglich ist, können diese Fehler umgangen werden, so dass Programm nur die fehlerträchtige Funktion beendet, aber dennoch nicht komplett abstürzt. Das .NET Framework von Microsoft bietet beispielsweise ein einheitliches Fehlermanagement mit der Möglichkeit zur Ausnahmebehandlung.

→ WEITERLESEN

Rekursion in der Programmierung

Die Rekursion und Iteration sind zwei konkurrierende Art und Weisen der Wiederholung eines Vorgangs in der Programmierung.
Während die Iteration eine Menge an Vorgängen (typischerweise eingerückt in eine Schleife) wiederholt und ggf. die Anzahl der Wiederholungen zählt (For-Schleife), ist die Rekursion ein Funktionsaufruf der selben Funktion – d.h. eine Funktion ruft sich selbst auf.

I.d.R. wird Rekursion für Suchalgorithmen verwendet. Die rekursiv arbeitende Funktion wird dabei so oft von sich selbst aufgerufen, bis ein Suchmuster gefunden wurde oder die Suche aus anderen Gründen endet.
Genauso wie bei der Iteration, ist auch bei der Rekursion darauf zu achten, dass es eine Abbruchbedingung gibt – Sonst droht eine Endlosschleife.

Die Rekursion hat gegenüber der Iteration einige Nachteile. Die Rekursion ist langsamer, da bei jedem rekursiven Schritt eine Methode aufgerufen wird. Aus diesem Grund belegt die Rekursion außerdem mehr Speicherplatz.

Dennoch haben sich rekursive Methoden in bestimmten Bereichen durchgesetzt. Z.B. als eleganter Lösungsweg, um ein mathematisches Problem zu lösen. Auch bei der Suche in Binären Bäumen hat sich die Rekursion durchgesetzt, da über eine iterative Vorgehensweise sehr viele Schleifen notwendig wären.

Die Rekursion erleichtert die Nachvollziehbarkeit von komplexen Suchalgorithmen, für Neulinge ist die Rekursion jedoch anfangs nur schwer vorstellbar, jedoch auch nur eine Sache der Gewohnheit. Aber auch für geübte Programmierer, können besonders verschachtelte, rekursive Methoden schnell sehr verwirrend sein.

→ WEITERLESEN

Wertetyp vs Refernztyp

In der prozeduralen und objektorientierten Programmierung wird zwischen Wertetypen und Referenztypen unterschieden.
Beispielsweise sind in den Programmiersprachen C# und Java primitive Datentypen, Enumeratoren und Strukturen Wertetypen. Klassenobjekte sind hingegen Referenztypen.
In C/C++ dienen Pointer (Zeiger) als Refernztyp, welche die Adresse speichert, die auf einen veränderbaren Wert verweist. Über den Pointer kann die Adresse dann von Funktion zu Funktion überreicht werden.
So kann aus jeder Funktion heraus, die den Zeiger zur Verfügung hat, der Wert verändert werden, auf den die Adresse zeigt. Der geänderte Wert ist über diese Adresse wieder abrufbar, von allen Funktionen aus, die die Refernz bekommen haben. Ein Zeiger in C ist also eine Referenz.

Ähnlich verhält es sich bei Klassenobjekten in C# und Java. Für diese wird ein Speicherplatz im Heap des Arbeitsspeichers angefordert, welcher sich über die Speicheradresse ansprechen und verändern (bzw. überschreiben) lässt. Auch die Klassenobjekte in C++ werden im Heap gespeichert.
Der Heap kann beliebig angesprochen werden, jedoch muss der Speicher bei Bedarf geordert und spätestens bei Programmende wieder freigegeben werden.
In C++ muss sich darum der Programmierer selbst kümmern; C#, Java und die meisten objektorientierten Skriptsprachen haben hierfür eine automatische Speicherbereinigung (Garbage Collector).

Wertetypen werden nicht über eine Speicheradresse an eine Funktion übergeben, sondern als Wert. Diese Werte werden kopiert und auf den Stack im Arbeitsspeicher gelegt.
Je mehr die Funktionsaufrufe verschachtelt sind, um so größer wird auch der Stack.
Diese Vorgehensweise ist durchaus sinnvoll, denn auf den Stack kann zwar nicht beliebig, sondern nur über das aktuellste (oberste) Stack-Element zugegriffen werden, da jedoch auch vor allem die
Funktionsvariablen (Parameter) benötigt werden, erfolgt der Zugriff daher i.d.R. sehr schnell.
Bei jeder Parameterübergabe mit Wertetypen werden die Werte daher kopiert und der Stack wächst. Die Werte sind (zumindest noch bei Funktionsaufruf) redundant auf dem Stack.
Werden die Parameter in der Funktion verändert, hat dies keine Auswirkungen auf die Variablen, welche außerhalb der Funktion als Parameter übergeben wurden.

In einigen Fällen ist es vom Programmierer erwünscht, dass auch Wertetypen innerhalb der Funktion geändert werden können. Wertetypen können als Referenz übergeben werden. Dann sind diese in C/C++ als Pointer zu übergeben, in C# und Java mit dem Schlüsselwort ref.

Ein Beispiel-Programm mit Funktionsaufrufen und Parameterübergaben als Werte- und Referenztyp:

[csharp] class Program
{
static void Main(string[] args)
{
Program program = new Program();
}

public Program()
{
int a = 45;
int b = 34;

this.Sum(a, b);
Console.WriteLine(„a = “ + a.ToString() + „; b = “ + b.ToString());

this.Sum2(ref a, ref b);
Console.WriteLine(„a = “ + a.ToString() + „; b = “ + b.ToString());
}

// Normalfall: By Value – die Variable a kann nicht überschrieben werden
private void Sum(int param1, int param2)
{
param1 = param1 + param2;
}

// Sonderfall: By Reference – die Variable a wird überschrieben
private void Sum2(ref int param1, ref int param2)
{
param1 = param1 + param2;
}
}
[/csharp]

Ausgabe:

a = 45; b = 34
a = 79; b = 34

Die Ausgabe des Programms zeigt, dass nur die Methode mit der als Referenz übergebenen Parameter die Variablen im Konstruktor verändert.

Alternativ zum Schlüsselwort ref kann auch das Schlüsselwort out verwendet werden.
Während ein mit ref bezeichneter Parameter vor Paramterübergabe definitiv initialisiert werden muss damit er verwendet werden kann, initialisiert sich ein mit out bezeichneter Parameter bei Methodenaufruf selber und muss daher nicht im Vorfeld initialisiert worden sein.
Hiervon abgesehen besteht kein wesentlicher Unterschied zwischen den Schlüsselwörtern ref und out, daher kann auch keine Überladung mit Unterscheidung dieser beiden Schlüsselwörter geschehen.

Boxing / Unboxing

Eine Konvertierung eines Wertetyps zu einem Referenztyp wird als Boxing bezeichnet. Unboxing ist die entgegengesetzte Konvertierung, also einer Referenz zu einem Wert. Dabei ist in C# und Java i.d.R. explizites Casting notwendig (Vorbestimmung des Datentyps zwischen einfachen Klammern).

[csharp] int a = 1548; // Wertetyp
object o = a; // boxing
int b = (int)o; // unboxing (mit Casting)
[/csharp]

Queue Datenstruktur in C#

Eine Queue (Warteschlange) ist eine First-in-First-out-(FIFO) Datenstruktur.
Die Datenstruktur Queue ist ein Stapel, auf welchem nach und nach Daten abgelegt werden.
Allerdings wird auf Elemente der Queue nur von unten zugegriffen. Das unterste Element ist das mit höchster Priorität, nur auf dieses als erstes hinzugefügte Element kann zugegriffen werden.

Stack

Ein Pointer (Zeiger) zeigt intern immer auf das Element, welches (relativ zu den anderen Elementen) als erstes in die Queue aufgenommen wurde.
Ein einfaches „Lösche aus der Queue“ gibt es nicht, ein Element kann aus der Queue mit Peek() herausgelesen und mit Dequeue() herausgelesen und zugleich entfernt werden.
An das Warteschlangenende angereiht werden Elemente mit Enqueue();

Funktionsweise einer Queue an Hand eines praktischen Beispiels:

Stellen Sie sich vor, Sie müssten eine Software für ein Theater schreiben. Die Software soll Ticket-Bestellungen für
Vorführungen bearbeiten. Nun sind die Vorstellungen aber sehr schnell ausgebucht, so dass viele Tickets nicht mehr ausgestellt werden können. Bei der Ticket-Bestellung gilt jedoch „wer zu erst kommt, mal zu erst“.
Die Daten aller eingehenden Bestellungen legen Sie in eine Queue (Warteschlange). Die Daten in der Queue werden vom ersten bis zum letzten Eintrag abgearbeitet, bis die Queue vollständig abgearbeitet wurde oder keine Plätze mehr verfügbar sind.

→ WEITERLESEN

Kategorien C#

Stack Datenstruktur in C#

Ein Stack (Stapel) ist eine Last-in-First-out-(LIFO) Datenstruktur.
Die Datenstruktur Stack sollte dann verwendet werden, wenn Daten gesammelt werden, jedoch immer nur ein Datenelement aktuell bearbeitet werden darf.

Das aktuelle Element ist dann das einzige, auf welches zugegriffen werden kann. Alle anderen Elemente bleiben im
Hintergrund und werden erst (und dann auch jeweils einzeln) bearbeitet, wenn das aktuelle Element abgearbeitet und vom Stapel entfernt wurde.

Ein Pointer (Zeiger) zeigt intern immer auf das Element, welches (relativ zu den anderen Elementen) als letztes auf den Stapel gelegt wurde.
Ein einfaches „Lösche aus dem Stack“ gibt es nicht, ein Element kann aus dem Stack mit Peek() herausgelesen und mit Pop() herausgelesen und zugleich entfernt werden.
Ein Element kann mit Push() auf den Stack gelegt werden.

Stack

Die Funktionsweise des Stack an Hand eines praktischen Beispiels:
Stellen Sie sich einen Stapel Teller vor, welchen sie abwaschen möchten. Sie würden sicherlich beim obersten Teller anfangen, diesen vom Stapel nehmen und abwaschen. Erst dann können Sie den nächsten Teller vom Stapel nehmen.

→ WEITERLESEN

Kategorien C#

Hashtable in C#

Hashtables (Hash-Tabellen) sind Datenstrukturen, die den Arrays sehr ähnlich sind.
Hashtables können in C# ähnlich wie eine ArrayList verwendet werden, der Zugriff erfolgt über einen Index.
Der Index ist ein spezieller Key, welcher eine Zahl (z.B. vom Typ Float oder Integer) genauso wie ein String sein darf.

[csharp] // Beispiele, Syntax: hashtablename[key] = value;

ht_pass[32.3] = „juhu“;
ht_pass[5] = „tobias“;
ht_pass[„zoo“] = 45.3;
[/csharp]

Wozu werden Hashtables benötigt?

Hashtables sind sinnvoll, wenn sich Listen bzw. Arrays verknüpfen bzw. verschachteln lassen. So kann der Wert eines Listenfeldes der Schlüssel zu einem Wert eines anderen Feldes sein. Zwar ließe sich das auch mit einem normalen Array
(und somit Integer-Werten als Index) realisieren, dann wären jedoch einige zusätzliche Listen/Tabellen nötig, um von einer
Zahl auf einen speziellen Schlüssel und von diesem Schlüssel wieder auf eine anderen Zahlenschlüssel zu verweisen.

C# bietet natürlich einen gewissen Komfort bei Benutzung von Hashtables (wie bei vielen anderen Dingen auch). In der Programmiersprache C haben Programmierer eigene Hash-Funktionen entwickelt, um z.B. einen Text als Zahl umzuschlüsseln (z.B. „Hallo“ -> „2342424“, „Schule“ -> „792942“ usw.), um diesen dann als Index (Schlüssel) für ein Array zu benuten. Vorteil: Um einen Wert zu suchen, musste nicht unbedingt das ganze Array durchlaufen und im
schlimmsten Falle alle Felder iteriert werden. Man musste sich nur die Schlüssel für bestimmte Werte merken, diese in eine Zahl konvertieren, auf den Wert im Feld mit dem Schlüssel zugreifen und es auslesen. Daraus ergibt sich ein sehr großer Geschwindigkeitsvorteil.
Der Nachteil liegt in der Größe des Arrays, dieses muss nämlich so groß sein bzw. so einen hohen Index haben, wie eine Zahl, die aus einem Text resultiert, werden kann. Außerdem könnte es vorkommen, dass Werte nicht abgespeichert werden können, da zwei oder mehrere unterschiedliche Texte trotz ausgereiften Algorithmus den selben Index ergeben. Ausweichmöglichkeiten sind zwar vorhanden (den Index verschieben, bis ein freies Feld gefunden wurde), macht die Sache jedoch trotzdem nicht viel unproblematischer.

Die Hashtables in C# sind weitaus dynamischer, denn intern ist ein Hahstable auch nur so groß, wie es notwendig ist. Der Zugriff ist jedoch absolut schnell, konstant schnell! Unabhängig von der Größe des Hashtable ist die Zugriffszeit konstant.

→ WEITERLESEN

Kategorien C#