Programmiersprache – C#

Die Programmiersprache C# (gesprochen: C Sharp) ist eine seit 2001 von Microsoft veröffentlichte Programmiersprache, welche sich seitdem auf Erfolgskurs zu einer der meist angewendeten Programmiersprachen entwickelt. C# ist international anerkannt und genormt. Neben C++ und Java gehört C# zu den mächtigsten und heute verbreitesten Hochsprachen. Während C++ eine hybride Sprache ist, sind Java und C# weitgehend objektorientierte Programmiersprachen.

Die Programmiersprache C# findet in Wissenschaft und vor allem in Wirtschaftsunternehmen verbreitete Anwendung und wird teilweise auch von Microsoft selbst für systemeigene Programme des Betriebssystems eingesetzt. In der Industrie, im Handel sowie in Dienstleistungsbranchen (wie z. B. der Versicherungsbranche) lassen sich mit C# entwickelte .Net-Anwendungen finden.

Mit C# lassen sich vielfältige Anwendungen realisieren, vom Dienstprogramm im Hintergrund über verteilte Anwendungen, Desktop- und Webanwendungen bis hin zur Smartphone-Applikation (App). C# Anwendungen laufen im Microsoft .Net Framework, welches so nur für Betriebssysteme von Microsoft existiert. C#-Quellcode wird anders als C++-Quellcode und ähnlich wie Java-Quellcode nicht einmalig direkt in hardware-spezifischen Maschinencode übersetzt, sondern zur Laufzeit durch einen sogenannten JIT-Compiler (Just-In-Time-Compiler). Über Betriebssysteme von Microsoft hinweg besteht eine sogenannte Plattformunabhängigkeit durch die Common Language Runtime (CLR) des .Net Frameworks. C#-Quellcode werden in eine Zwischensprache (Intermediate Language Code) übersetzt, was in ausführbare .exe-Dateien resultiert. Das Starten der .exe-Dateien führt dazu, dass der Just-In-Time-Compiler der CLR den Zwischencode in Maschinencode übersetzt. Ohne .Net-Framework sind diese .exe-Dateien nicht ausführbar.

→ WEITERLESEN

.NET-Code, unfreiwilliger Open Source?

Es ist schon ein wenig gewöhnungsbedürftig, der Gedanke, dass der gesamte Code eines mühevoll entwickeltes Programms quasi von jedem Nutzer eingesehen werden kann. „Open Source“ ist an dieser Stelle sicherlich zu viel gesagt, denn nur weil der Quellcode (mit Hilfsmitteln) einsehbar ist, heißt dies nicht, dass dieser auch für andere Zwecke als die Programmausführung genutzt werden darf.

Aber ein einsehbarer Quellcode erleichtert die Spionage, Piraterie und sonstigen Missbrauch enorm.
Aber warum kann der Quellcode eines .Net Programms so einfach eingesehen werden? Das Problem existiert bei .NET Sprachen (und auch Java) zum einen, weil die Quellcode in eine Zwischensprache (IL, Intermediate Language) übersetzt wird und zum anderen an Reflections.

Die Zwischensprache ist bereits für den Menschen lesbarer als Assemblercode und lässt sich verhältnismäßig einfach wieder zurück übersetzen, denn die Zwischensprache spricht anders als Assemblercode nicht einzelne Register an (kann sie auch nicht, denn sie ist eine hardware-universelle Sprache), sondern liest sich auf einem viel höheren Abstraktionslevel.

→ WEITERLESEN

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

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#