7 Weitere Möglichkeiten von C#
Wir haben nun alle wichtigen Facetten der Objektorientierung beleuchtet. Es gibt jedoch noch viele andere Themen, die in der Programmierung mit .NET eine wichtige Rolle einnehmen und denen wir uns in diesem Kapitel widmen. Dazu gehören beispielsweise Namensräume, Generics, die Überladung von Operatoren, Indexer, Erweiterungsmethoden, Lambda-Ausdrücke, Attribute, das dynamische Binden, die Deklaration von Zeigern u. v. m.
7.1 Namensräume (Namespaces) 

Die .NET-Klassenbibliothek enthält zahlreiche Klassendefinitionen, die dem Entwickler im Bedarfsfall ihre individuellen Dienste über Methoden bereitstellen. Sie können davon ausgehen, dass sich das Angebot im Laufe der Zeit durch neue Technologien noch deutlich erweitern wird. Dabei sind die benutzerdefinierten Klassen noch nicht berücksichtigt.
Gäbe es für dieses große Angebot keine besondere Verwaltungsstruktur, wäre das Chaos perfekt. Erfahrene Entwickler wissen, wie schwierig es ist, aus den circa 5000 bis 6000 verschiedenen Betriebssystemfunktionen eine bestimmte zu finden. Da hilft auch kein von Microsoft sorgfältig gewählter, beschreibender Funktionsname weiter: Die Suche gleicht dem Stöbern nach der berühmten Stecknadel im Heuhaufen.
Dieser Problematik waren sich die .NET-Architekten bewusst und haben daher das Konzept der Namespaces (Namensräume) eingeführt. Namespaces sind hierarchische, logische Organisationsstrukturen. Sie kategorisieren Typdefinitionen, um das Auffinden einer bestimmten Funktionalität auf ein Minimum an Aufwand zu reduzieren und Mehrdeutigkeiten zu vermeiden.
Namespaces lassen sich sehr gut mit der Ordnerstruktur eines Dateisystems vergleichen. Dabei ähnelt ein Namespace einem Verzeichnis. Jedes Verzeichnis enthält Dateien, die meist logisch miteinander in Beziehung stehen: Beispielsweise können die Dateien eine Anwendung bilden, oder es handelt sich um gemeinsam verwaltete Benutzerdokumente. Innerhalb eines Namespace werden ebenfalls logisch zusammenhängende Typen verwaltet. Beim Vergleich mit dem physikalischen Dateisystem entspricht eine Typdefinition einer Datei. Innerhalb eines Ordners muss der Name einer Datei eindeutig sein – innerhalb eines Namespace gilt dasselbe für die Typbezeichner. Im Dateisystem können Verzeichnisse Unterverzeichnisse enthalten, um eine feinere Gliederung zu erzielen. Aus denselben Gründen können Namespaces weitere Namespaces einbetten.
Ein Namespace ist ein Verwaltungskonstrukt, in dem ein oder mehrere Typen logisch gruppiert werden, die funktional in einer verwandtschaftlichen Beziehung stehen. Beispielsweise sind alle Klassen des .NET-Frameworks, die Dateioperationen zur Verfügung stellen, dem Namespace System.IO zugeordnet. Der größte Namespace ist der mit der Bezeichnung System. Er enthält die wichtigsten .NET-Typen und hat aus organisatorischen Gründen weitere, untergeordnete Namespaces.
Zwischen einem Namespace und einer Bibliotheksdatei (DLL), die Typdefinitionen enthält, besteht keine 1:1-Beziehung. Vielmehr kann sich ein Namespace über mehrere DLL-Dateien erstrecken. Umgekehrt können in einer DLL-Datei auch mehrere Namespaces definiert werden.
Grundsätzlich ist jeder Typ Mitglied eines Namespace. Folgerichtig wird auch jedweder Programmcode in Namespaces verwaltet. Jedes neue Projekt eröffnet dazu einen neuen Namespace, in dem alle Typen des aktuellen Projekts verwaltet werden.
7.1.1 Zugriff auf Namespaces 

Es ist ein Irrtum zu glauben, man könne ohne weitere Maßnahme auf jeden beliebigen Namespace und eine darin verwaltete Klasse Zugriff erhalten. Vielmehr muss dem Projekt die Datei bekannt gegeben werden, die den erforderlichen Namespace enthält.
Damit jedes Projekt von Anfang an eine gewisse Grundfunktionalität hat, werden die wichtigsten Bibliotheken von Anfang an eingebunden. Sie finden die Liste der Dateiverweise im Projektmappen-Explorer, wenn Sie den Knoten Verweise im Projektmappen-Explorer öffnen. Die Dateiendung DLL wird in der Verweisliste nicht mit angegeben, da es sich nur um DLLs handeln kann (siehe Abbildung 7.1).
Abbildung 7.1 Der geöffnete Knoten »Verweise«
Damit stehen dem Entwickler schon beim Öffnen eines neuen Projekts sehr viele Klassen zur Verfügung – nämlich die, die in den Bibliotheken enthalten sind, auf die verwiesen wird. Sollte es sich im Laufe der Entwicklungszeit herausstellen, dass darüber hinaus noch weitere benötigt werden, muss die Verweisliste um die entsprechenden Bibliotheken ergänzt werden. Dazu öffnen Sie das Kontextmenü des Knotens Verweise im Projektmappen-Explorer und wählen Verweis hinzufügen... Daraufhin wird das Dialogfenster aus Abbildung 7.2 angezeigt. In der Registerkarte .NET wird die gewünschte Datei markiert und über die Schaltfläche OK zur Liste der ausgewählten Komponenten hinzugefügt. In Tabelle 7.1 sind alle Registerkarten des Dialogs erläutert.
Abbildung 7.2 Der Dialog zum Hinzufügen von Verweisen
Registerkarte | Beschreibung |
.NET |
Hier wählen Sie die Bibliotheken aus, die sich an zentraler Stelle (Global Assembly Cache – GAC) eingetragen haben. Diese Lokalität wird durch den Pfad \Windows\assembly beschrieben. |
COM |
Möchten Sie eine Komponente nutzen, die für COM/ActiveX entwickelt worden ist, suchen Sie die gewünschte Komponente hier. |
Projekte |
Hier werden Ihnen die Projekte zur Auswahl angeboten, die sich in derselben Projektmappe mit dem aktuellen Projekt befinden. |
Durchsuchen |
Ist die Bibliothek nicht im Global Assembly Cache eingetragen, können Sie über diese Lasche zum Speicherort der DLL-Datei navigieren. |
Aktuell |
Hier finden Sie eine Liste derjenigen Bibliotheken, die Sie während Ihrer letzten Sessions hinzugefügt haben. |
Wenn Sie wissen, welche Klasse Sie in Ihrem Projekt benötigen, stellt sich nur noch die Frage, in welcher Datei die Klasse zu finden ist. Die Lösung ist sehr einfach, wenn Sie sich das Datenblatt der entsprechenden Klasse in der .NET-Dokumentation ansehen. Darin werden Sie sowohl die Angabe des Namespace finden, dem die Klasse zugeordnet ist, als auch die Angabe der zugehörigen Bibliotheksdatei.
7.1.2 Die »using«-Direktive 

Standardmäßig muss beim Zugriff auf eine Klasse auch der Namespace angeführt werden, dem die Klasse zugeordnet ist. Betrachten wir dazu das schon häufig benutzte Beispiel der Methode WriteLine der Klasse Console, die zum Namespace System gehört. Um im Konsolenfenster eine Ausgabe zu erhalten, müssten Sie streng genommen
System.Console.WriteLine("Hallo Welt");
schreiben. Eine Angabe, die aus Namespace und Klassenname besteht, wird als vollqualifizierter Name bezeichnet und ähnelt einer kompletten Pfadangabe im physikalischen Dateisystem. Vollqualifizierte Namen führen oft zu sehr langen, unübersichtlichen und schlecht lesbaren Ausdrücken im Programmcode, insbesondere wenn mehrere Namespaces ineinander verschachtelt sind. C# bietet uns mit der using-Direktive Abhilfe. Mit
using System;
können Sie an späterer Stelle im Programmcode auf alle Typen des so bekannt gegebenen Namespace unter Angabe des Typbezeichners zugreifen, ohne den vollqualifizierten Namen angeben zu müssen:
Console.WriteLine("Hallo Welt");
using-Direktiven stehen außerhalb der Klassendefinitionen und beziehen sich nur auf die Quellcodedateien, in denen sie angegeben sind.
7.1.3 Globaler Namespace 

In .NET gibt es einen sogenannten globalen Namespace. Diesem werden die folgenden Elemente zugeordnet:
- alle Toplevel-Namespaces
- alle Typen, die keinem Namespace zugeordnet sind
Der Zugriff auf den globalen Namespace unterliegt einer speziellen Syntax und wird in Abschnitt 7.1.6, »Der ::-Operator«, erläutert.
7.1.4 Vermeiden von Mehrdeutigkeiten 

Namespaces dienen zur Strukturierung und Gruppierung von Klassen ähnlicher Merkmale, aber auch zur Vermeidung von Mehrdeutigkeiten. Konflikte aufgrund gleicher Typbezeichner werden durch Namespaces vermieden. Allerdings kann die Bekanntgabe mehrerer Namespaces mit using Probleme bereiten, sollten in zwei verschiedenen Namespaces jeweils gleichnamige Typen existieren. Dann hilft using auch nicht weiter. Angenommen, in den beiden fiktiven Namespaces MyApplication und YourApplication wäre jeweils eine Klasse Person definiert, dann würde das folgende Codefragment wegen der Uneindeutigkeit des Klassenbezeichners einen Fehler verursachen:
using MyApplication; using YourApplication; class Demo { static void Main(string[] arr) { Person obj = new Person(); ... } }
Die Problematik lässt sich vermeiden, wenn der Namespace der Klasse Person näher spezifiziert wird, beispielsweise mit:
MyApplication.Person person = new MyApplication.Person();
Es gibt auch noch eine weitere Möglichkeit, um den Eindeutigkeitskonflikt oder eine überlange Namespace-Angabe zu vermeiden: die Definition eines Alias. Während die einfache Angabe ohne Alias hinter using nur einen Namespace erlaubt, ersetzt ein Alias den vollständig qualifizierenden Typbezeichner. Damit könnten die Klassen Person in den beiden Namespaces auch wie folgt genutzt werden:
using FirstPerson = MyApplication.Person; using SecondPerson = YourApplication.Person; ... FirstPerson person = new FirstPerson();
Genauso können Sie, falls Sie Spaß daran haben, die Klasse Console umbenennen, z. B. in Ausgabe:
using Ausgabe = System.Console; ... Ausgabe.WriteLine("Hallo Welt");
7.1.5 Namespaces festlegen 

Jedem neuen C#-Projekt wird von der Entwicklungsumgebung automatisch ein Namespace zugeordnet. Standardmäßig sind Namespace- und Projektbezeichner identisch.
Solange sich Typen innerhalb desselben Namespace befinden, können sie sich gegenseitig direkt mit ihrem Namen ansprechen. Die Klassen DemoA, DemoB und DemoC des folgenden Codefragments sind demselben Namespace zugeordnet und benötigen deshalb keine vollqualifizierte Namensangabe.
namespace MyApplication {
class DemoA {/*...*/}
class DemoB {/*...*/}
class DemoC {/*...*/}
}
Jeden Namespace können Sie selbstverständlich nach eigenem Ermessen benennen. Häufig verwenden die Unternehmen dazu Ihren Unternehmensnamen. Zudem lassen sich auch mehrere Namespaces angeben, wie das folgende Codefragment zeigt:
using System; using MyApp; using ConsoleApplication; namespace ConsoleApplication { class Program { static void Main(string[] args) { // erfordert: using MyApp; Demo obj = new Demo(); } } } namespace MyApp { public class Demo { public void Test() { // erfordert: using ConsoleApplication; Program obj = new Program(); } } }
Das Beispiel zeigt die beiden parallelen Namespaces ConsoleApplication und MyApp. Jeder enthält eine Klasse mit einer Methode, in der ein Objekt vom Typ der Klasse aus dem anderen Namespace instanziiert wird. Da der Zugriff Namespace-übergreifend ohne die Angabe des vollqualifizierten Bezeichners erfolgt, müssen beide Namespaces durch using bekannt gegeben werden.
Eingebettete Namespaces
Ein Namespace kann mit einem Ordner des Dateisystems verglichen werden. So wie ein Ordner mehrere Unterordner enthalten kann, können auch Namespaces eine hierarchische Struktur bilden. Der oberste Namespace, der entweder dem Projektnamen entspricht oder manuell verändert worden ist, bildet die Wurzel der Hierarchie, ähnlich einer Laufwerksangabe.
Soll dieser Stamm-Namespace eine feinere Strukturierung aufweisen und eingebettete Namespaces verwalten, wird innerhalb eines Namespace ein weiterer, untergeordneter Namespace definiert:
namespace Outer { class DemoA { static void Main(string[] args) { DemoB obj = new DemoB(); } } namespace Inner { class DemoB { public void TestProc() {/*...*/} } } }
Ein Typ in einem übergeordneten Namespace hat nicht automatisch Zugriff auf einen Typ in einem untergeordneten Namespace. Damit das Codefragment auch tatsächlich fehlerfrei kompiliert werden kann, ist es erforderlich, mit
using Outer.Inner;
den inneren Gültigkeitsbereich den Typen in der übergeordneten Ebene bekannt zu geben.
7.1.6 Der ::-Operator 

Auch für Namespaces lässt sich ein Alias festlegen, beispielsweise:
using EA = System.IO;
Sie können nun wie gewohnt den Punktoperator auf den Alias anwenden, also:
EA.StreamReader reader = new EA.StreamReader("...");
Seit dem .NET Framework 2.0 bietet sich aber auch die Möglichkeit, mit dem ::-Operator auf Typen aus Namespace-Aliasen zu verweisen.
EA::StreamReader reader = new EA::StreamReader("...");
Die Einführung des ::-Operators hatte den Grund, unschöne Effekte zu vermeiden, die sich im Zusammenhang mit Namespace-Aliasen und dem Punkt-Operator ergeben können. Sehen Sie dazu den folgenden Beispielcode an:
using System; using Document = Tollsoft.Developement.Office; namespace ConsoleApplication { class Program { static void Main(string[] args) { Document.Demo demo = new Document.Demo(); } } } namespace Tollsoft.Developement.Office { class Demo { } }
Richten Sie Ihr Augenmerk auf die Anweisung in der Methode Main. Die Syntax Document.Demo lässt nicht eindeutig erkennen, ob es sich bei Document um einen Namespace handelt oder um einen Namespace-Alias. Zudem wäre auch noch denkbar, dass Demo eine innere Klasse von Document ist. Die Verwendung des ::-Operators würde zumindest demjenigen Entwickler eine Hilfe sein, der sich in den Quellcode neu einarbeiten muss. Besser wäre also die folgende Anweisung:
Document::Demo demo = new Document::Demo();
Noch bedeutender wird der ::-Operator, wenn in einer anderen Assembly, auf die im Projekt verwiesen wird, ein Namespace oder ein Typ mit dem gleichen Namen wie der Alias angeboten wird. Die Syntax Document.Demo würde dann sogar zu einem Fehler führen, während Document::Demo eindeutig ist.
Hinweis |
Man sollte diese Überlegung nicht einfach vom Tisch wischen, wenn aktuell kein Konflikt mit einem Namespace oder Typ in einer anderen Assembly vorliegt. Möglicherweise wird die Assembly, auf die verwiesen wird, später in einer neueren Version ausgeliefert. Spätestens dann könnte es zu einem Eindeutigkeitskonflikt kommen. |
Der ::-Operator gestattet auch den Zugriff auf den globalen Namespace. Dazu müssen Sie nur das C#-Schlüsselwort global vor den Namespace setzen. Auf diese Weise können Sie auf Typen zugreifen, die Sie nicht explizit einem Namespace zugeordnet haben (siehe Abbildung 7.3).
Abbildung 7.3 Der globale Namespace
Halten wir an dieser Stelle Folgendes zum Einsatz des ::-Operators fest:
- Der ::-Operator ist notwendig, um mithilfe von global auf den globalen Namespace zuzugreifen.
- Der ::-Operator sollte benutzt werden, um bei der Verwendung eines Namespace-Alias Eindeutigkeitskonflikte zu vermeiden.