Startseite  Index  <<  >>  

6 - Namespaces

6.1 Namespaces deklarieren

6.1.1 Einführung

Wenn Sie umfangreichere Anwendungen mit .NET erstellen, fällt einer gut gewählten Namensgebung sowie der geschickten Verwaltung von Klassen und anderen Typen eine größere Rolle zu. In einfachsten Anwendungen verwenden Sie vielleicht nur eine einzige oder wenige Klassen, deren Deklarationen ohne die Verwendung von Namespaces (Namensräume) auskommen.

Beispiel: Klassendeklaration ohne Namespace

using System;
public class TestAusgabe
{
  public void Ausgabe()
  {
    Console.WriteLine("Ohne Namespace...");
  }
}
Listing 6.1: OhneNamespace.cs (Projekt MitUndOhneNamespaceProj)

Wenn Sie eine solche Klasse ansprechen möchten, verwenden Sie direkt den Klassennamen, z.B. TestAusgabe. Jetzt kann es allerdings passieren, dass ein anderer Entwickler auf die gleiche Idee kommt und seine Klasse genauso benennt wie Sie und diese ebenfalls in keinen Namespace einbettet. Sie haben jetzt keine Möglichkeit mehr, beide Klassen gleichzeitig zu verwenden ohne in einen Namenskonflikt zu gelangen. Des Weiteren steht Ihnen nur der Klassenname als Gedankenstütze zur Verfügung, um das Einsatzgebiet der Klasse zu bestimmen.

Aus den genannten Gründen ist es von Vorteil, Klassen und andere Typen zusätzlich in einem Namespace (Namensraum) unterzubringen. Der Klassenname muss jetzt nur noch innerhalb des Namespaces eindeutig sein. Außerdem kann der Name des Namespaces dazu dienen, den Einsatzort (z.B. das Projekt) der Klasse genauer zu spezifizieren. Eine Klasse zum Umrechnen von Währungen kann sich z.B. im Namespace Waehrungen (oder Currencies) befinden.

Beispiel: Eine Klasse in einen Namespace einbetten

Die Klasse TestAusgabeNS wird im Namespace MitNamespace untergebracht. Der vollständige Name der Klasse lautet nun MitNamespace.TestAusgabeNS.

using System;
namespace MitNamespace
{
  public class TestAusgabeNS
  {
    public void Ausgabe()
    {
      Console.WriteLine("Mit Namespace...");
    }
  }
}
Listing 6.2: MitNamespace.cs (Projekt MitUndOhneNamespaceProj)

Um die Klasse TestAusgabe zu verwenden, ist keine weitere Aktion notwendig. Entweder Sie binden die Source-Datei oder die Assembly in das Projekt ein. Anders sieht es beim Zugriff auf die Klasse TestAusgabeNS aus. Entweder Sie binden den Namespace MitNamespace über die using-Anweisung ein (jetzt können Sie wieder direkt den Klassennamen verwenden) oder Sie verwenden die Klasse mit ihrem voll qualifizierten Namen MitNamespace.TestAusgabeNS. Letzteres ist zum Beispiel dann notwendig, wenn sich eine gleichnamige Klasse in mehreren eingebundenen Namespaces befindet.
using System;
using MitNamespace;
namespace CSharpBuch.Kap06
{
  public class MitUndOhneNamespace
  {
    public static void Main(string[] args)
    {
      new TestAusgabe().Ausgabe();
      // verkürzte Schreibweise, da Namespace eingebunden
      new TestAusgabeNS().Ausgabe();
      // vollständig qualifizierte Schreibweise 
      new MitNamespace.TestAusgabeNS().Ausgabe();
      Console.ReadLine();
    }
  }
}
Listing 6.3: MitUndOhneNamespace.cs (Projekt MitUndOhneNamespaceProj)

Namespaces deklarieren also Gültigkeitsbereiche. In einem Namespace können Sie folgende Typen deklarieren:

  • Weitere Unter-Namespaces (dadurch entstehen verschachtelte Namespaces wie z.B. System.Text, die durch einen Punkt voneinander getrennt werden)
  • Klassen (class)
  • Schnittstellen (interface)
  • Strukturen (struct)
  • Aufzählungen (enum)
  • Delegaten (delegate) Wird kein Namespace verwendet, wird dieser als globaler, unbenannter oder Standard-Namespace bezeichnet. Die darin deklarierten Bezeichner können überall verwendet werden.

    INFO

    Natürlich können Sie statt eines Namespaces den Klassennamen aus dem Namen des Namespaces und dem Klassennamen zusammensetzen. Der Quellcode würde allerdings auf diese Weise schlechter lesbar sein, wenn statt dem Klassennamen Mathe der Name CSharpBuchKap06Mathe verwendet werden würde. Außerdem wäre dadurch die Nutzung der Klasse CSharpBuchKap06Mathe im Kapitel 5 aufgrund ihres Namens sehr verwirrend.

    6.1.2 Verschachtelte Namespaces

    Namespaces können auch verschachtelt werden. Dadurch lassen sich Hierarchien von Gültigkeitsbereichen erzeugen. Außerdem können Sie Klassen und andere Typen noch besser nach ihrem Einsatzgebiet strukturieren. Damit sich Ihre Namespace-Deklarationen nicht mit denen anderer Entwickler oder anderer Firmen überschneiden, können Sie in der ersten Ebene z.B. Ihren Firmennamen verwenden.

    NamespaceEinsatzgebiet der darin enthaltenen Typen
    DFSoftware.Utils.MatheHilfsmethoden für mathematische Operationen
    DFSoftware.TaschenrechnerHauptanwendung - ein Taschenrechner
    DFSoftware.Web.UpdateMethoden, um die Anwendung über das Web zu aktualisieren
    Tabelle 6.1: Beispiele für verschachtelte Namespace-Deklarationen

    Beispiel: Namespaces zur besseren Strukturierung verschachteln

    Innerhalb des Namespaces CSharpBuch werden sämtliche anderen Typen und Namespaces untergebracht, die irgendetwas mit diesem Buch zu tun haben. Zum einfacheren Auffinden der Beispiele, wird für jedes Kapitel ein eigener Unter-Namespace mit dem Kapitelnamen verwendet. Es wäre es auch möglich gewesen, nochmals pro Abschnitt einen eigenen Namespace Unterkapitel mit einer fortlaufenden Nummerierung zu deklarieren. Über den Projektnamen sind die Anwendungen aber bereits ausreichend eindeutig gekennzeichnet.

    using System;
    namespace CSharpBuch
    {
      namespace Kap06
      {
        namespace Unterkapitel
        {
          public class Test
          {
            static void Main(string[] args)
            {
              // Kurzschreibweise von 
              // Test t =  new CSharpBuch.Kap06.Unterkapitel.Test();
              // t.Ausgabe();
              new CSharpBuch.Kap06.Unterkapitel.Test().Ausgabe();
              // natürlich könnte die Klasse Test auch unqualifiziert
              // verwendet werden
              // new Test().Ausgabe();
            }
            public void Ausgabe()
            {
              Console.WriteLine("Verschachtelung von Namespaces");
              Console.ReadLine();
            }
          }
        }
      }
    }
    Listing 6.4: VerschachtelteNamespaces.cs

    6.1.3 Syntax von Namespaces

  • Ein Namespace wird durch das Schlüsselwort namespace eingeleitet. Zugriffsmodifizierer kommen bei Namespaces nicht zum Einsatz. Ihr Inhalt ist immer public.
  • Danach folgt der Name des Namespaces, der ein gültiger Bezeichner sein muss. Der Bezeichner besteht dabei aus einem oder mehreren Hauptworten, wobei jedes Hauptwort groß geschrieben wird.
  • Der Inhalt eines Namespaces wird in geschweifte Klammern eingeschlossen.
  • Verschachtelte Namespaces entstehen entweder durch die Verschachtelung mehrerer Namespace-Deklarationen oder durch die Verwendung eines Punkts im Namen des Namespaces. Der Punkte trennt dabei jeweils einen Bestandteil verschachtelter Namespaces. Statt
    namespace CSharpBuch
    {
      namespace Kap06
      {
        namespace Unterkapitel
        {

    lässt sich auch verkürzt schreiben:

    namespace CSharpBuch.Kap06.Unterkapitel
    {

    Innerhalb einer C#-Datei können Sie auch mehrere Namespace-Deklarationen verwenden. Allerdings ist dies unüblich, da pro Datei in der Regel nur ein Typ definiert wird.
    namespace Namespace1
    {}
    namespace Namespace2
    {}
    Ein Namespace kann sich durchaus über mehrere Dateien und Assemblies erstrecken. So können Sie beispielsweise eine Assembly erstellen, welche die wichtigsten Klassen eines Namespaces umfasst und eine weitere Assembly, die spezielle Hilfsklassen enthält. Der Inhalt des Namespaces ist die Summe aller Typdefinitionen in allen diesen Dateien. In diesem Fall müssen Sie darauf achten, dass jeder Bezeichner nur einmal innerhalb des Namespaces vorkommt. Ansonsten erhalten Sie einen Compilerfehler, da der Typname nicht mehr eindeutig ist.

    INFO

    Der Name eines Namespaces ist völlig unabhängig vom Namen oder dem Speicherort einer Datei. So kann sich der Inhalt eines Namespaces aus mehreren Dateien zusammensetzen, die an völlig unterschiedlichen Orten gespeichert sind. Namespaces dienen nur dazu, die darin enthaltenen Typen zu strukturieren.

    6.1.4 Namensgebung

    Der Name eines Namespaces sollte den Einsatzzweck der enthaltenen Typen kennzeichnen. In diesem Fall sollte der Name im Plural geschrieben werden. Enthält Ihr Namespace Klassen zur Matrizenrechnung, könnte er DFSoftware.Matrizen heißen.

    Namespaces werden wie Typnamen in der Pascalschreibweise bezeichnet, d.h. jedes Hauptwort beginnt mit einem Großbuchstaben. Verwenden Sie möglichst einen mehrstufigen Namespace für Ihre Typen, der z.B. aus dem Firmen-, Familien- oder Produktnamen und mindestens einer Unterkategorie besteht. Hilfsklassen können Sie z.B. in einem Namespace Util (für Utility - Hilfsklassen) unterbringen. Die Klassen einer Anwendung gelangen dann in einen Namespace, der dem Namen der Anwendung entspricht, z.B.
    DFSoftware.DFWord
    DFSoftware.Taschenrechner

    6.2 Namespaces einbinden

    Damit man auf den Inhalt eines Namespaces zugreifen kann, müssen Sie die betreffende C#-Datei in Ihr Projekt einbinden oder dem Projekt eine Referenz auf die implementierende Assembly hinzufügen (Rechtsklick auf den Ordner VERWEISE im Projekt und Auswahl des Kontextmenüpunkts VERWEISE HINZUFÜGEN). Jetzt können Sie schon einmal auf den Inhalt des Namespaces zugreifen.

    Damit Sie nicht jedes Mal den voll qualifizierten Namen einer Klasse oder eines anderen Typs verwenden müssen, der sich in einem Namespace befindet, machen Sie den Namespace mittels der using-Direktive bekannt.
    // voll qualifizierten Namen verwenden
    CSharpBuch.Kap06.Test t = new CSharpBuch.Kap06.Test();
    
    // oder Namespace einbinden
    using CSharpBuch.Kap06;
    ...
    Test t = new Test();
    Der Direktive using folgt der vollständige Name des Namespaces. Hierbei ist zu beachten, dass exakt nur der angegebene Namespace eingebunden wird. Die Reihenfolge bei der Verwendung mehrerer using-Direktiven ist nicht signifikant. Mit anderen Worten - eine using-Direktive hat keinen Einfluss auf eine andere using-Direktive. Besitzt der Namespace weitere verschachtelte Unter-Namespaces, müssen diese separat eingebunden werden. Die Verwendung von Platzhalterzeichen wie dem Stern * ist nicht möglich.
    using System;
    using System.Text;

    Die using-Direktive kann sich außerhalb oder innerhalb einer Namespace-Deklaration befinden. Befindet sie sich darin, ist sie auch nur im Gültigkeitsbereich des Namespaces aktiv. Dies ist die bevorzugte Schreibweise laut Code-StyleGuide. Allerdings befindet sich in einer Datei normalerweise sowieso nur ein Namespace, so dass der Autor es bevorzugt die using-Anweisungen immer zu Beginn vor dem Namespace anzugeben.

    Die using-Direktive muss sich aber in jedem Fall vor allen anderen Typdefinitionen befinden.

    Beispiel: Einsatz der using-Direktive

  • Globale Gültigkeit der using-Direktive:
    using System;
    namespace CSharpBuch
    {
  • Gültigkeit von using nur innerhalb des Namespaces und Unter-Namespaces
    namespace CSharpBuch
    {
      using System;
  • Fehler, using muss vor allen Typdefinitionen stehen
    namespace CSharpBuch
    {
      public class Test ...
      ...
      using System; // Fehler

    INFO

    Der Zugriff auf einen Typ kann nur über den voll qualifizierten Namen oder den reinen Typnamen (unqualifizierter Name) erfolgen. Es erfolgt keine Zusammensetzung von unvollständigen Bezeichnern
    using System;
    ...
    // Der Compiler erzeugt einen Fehler, da es keinen Namespace
    // Text gibt. Eine automatische Zusammensetzung von 
    // System.Text.StringBuilder ist nicht möglich
    Text.StringBuilder sb = new Text.StringBuilder();

    6.2.1 Aliase

    Beim Einbinden mehrerer Namespaces kann es nun vorkommen, dass derselbe Typname in mehreren Namespaces definiert ist. Der Zugriff auf den Typ kann in diesem Fall nicht ohne Angabe des vollständigen Namespaces vor dem Typnamen erfolgen. Um dennoch Schreibarbeit zu vermeiden, können Sie Aliase für Namespaces und deren Typen erzeugen. Der using-Direktive folgt zur Definition eines Alias ein Bezeichner und mit einem Gleichheitszeichen getrennt der Name eines Namespaces oder eines voll qualifizierten Typs. Die Definition eines Alias muss vor allen Typdeklarationen erfolgen, also wie beim üblichen Gebrauch der using-Direktive.
    using K6Test = CSharpBuch.Kap06.Test;
    using Kap6 = CSharpBuch.Kap06;
    ...
    K6Test t = new K6Test();
    Kap6.Test t = new Kap6.Test();

    INFO

    Befinden Sie sich in einem bestimmten Namespace, können Sie implizit auf alle Bezeichner über deren unqualifizerten Namen zugreifen, die in diesem Namespace definiert werden.

    ACHTUNG

    Das Einbinden zweier Namespaces, die den gleichen Bezeichner enthalten erzeugt noch keinen Compiler-Fehler. Erst die unqualifizierte Verwendung eines Bezeichners wird vom Compiler bemängelt, da er nicht feststellen kann, welcher gemeint ist. Ein Ausweg besteht in der Verwendung des voll qualifizierten Namens bei der Erzeugung eines Objekts.
    using NameSpaceA; // enthält den Typ Test
    using NameSpaceB; // enthält auch den Typ Test
    ...
    Test t = new NameSpaceB.Test();

    Beispiel: Verwendung von Aliasnamen für Namespaces und Typen

    In der Anwendung wird für den Namespace MitNamespace und den Typ MitNamespace.TestAusgabe jeweils ein Alias definiert. Außerdem befindet sich der Typ TestAusgabe noch einmal ohne Namespace in der Anwendung. Über new wird dann dreimal ein Objekt vom betreffenden Typ erzeugt und sofort im Anschluss über den Punkt die Methode Ausgabe() aufgerufen. Dies ist also auch ein Beispiel, welches die Objekterzeugung mit einem sofortigen Methodenaufruf verbindet. Allerdings haben Sie in diesem Fall keinen Zugriff auf das Objekt, da es keiner Referenzvariablen zugewiesen wird. Zuerst wird die Methode Ausgabe() der Klasse TestAusgabe aufgerufen, die sich in keinem Namespace befindet. Dann wird zweimal die Methode Ausgabe() der Klasse verwendet, die im Namespace MitNamespace untergebracht ist.

    using System;
    using MN = MitNamespace;
    using MNC = MitNamespace.TestAusgabe;
    namespace NamespaceAliase
    {
      public class NamespaceAliase
      {
        public static void Main(string[] args)
        {
          new TestAusgabe().Ausgabe();
          new MN.TestAusgabe().Ausgabe();
          new MNC().Ausgabe();
          Console.ReadLine();
        }
      }
    }
    namespace MitNamespace
    {
      public class TestAusgabe
      {
        public void Ausgabe()
        {
          Console.WriteLine("Mit Namespace...");
        }
      }
    }
    public class TestAusgabe
    {
      public void Ausgabe()
      {
        Console.WriteLine("Ohne Namespace...");
      }
    }
    Listing 6.5: NamespaceAliase.cs

    Der Namespace global und der ::-Operator

    Bei der Suche nach einem Bezeichner beginnt der Compiler in der aktuellen Klasse, dann in den anderen Klassen des aktuellen Namespaces. Ist er darin nicht enthalten, werden die eingebundenen Namespaces durchsucht. Probleme treten dann auf, wenn Sie beispielsweise für die Member einer Klasse Bezeichner vergeben, die denen aus anderen eingebundenen Namespaces entsprechen. Diese Member der anderen Namespaces sind dann verborgen und können nur mit einem Zugriff über den globalen Namespace angesprochen werden. Diese Vorgehensweise steht ab der Version 2.0 des .NET Frameworks zur Verfügung.

    Der Bezeichner global steht für die Wurzel aller Typen und Namespaces der globalen Umgebung. Bei der Angabe von global links vom Namespace wird die Suche nach einem Typ im globalen (unbenannten) Namespace begonnen. Das .NET Framework referenziert global::System z.B. als den System-Namespace. Die Angabe von global ist nicht notwendig, wenn es keine Überlagerungen von Namen gibt. Wenn Sie jedoch auf gleichnamige Member eines anderen Namespaces zugreifen möchten müssen Sie den voll qualifizierten Namen von global aus angeben.

    Der Namespace-Aliasqualifizierer :: (ein weiterer Operator) wird verwendet, um einen Alias (für Namespaces) oder den globalen Namensraum zu referenzieren.

    Beispiel: Verwendung des Namespace-Aliasqualifizierers

    Sie definieren in Ihrer Klasse zwei Variablen mit den Namen System und Console (was man natürlich tunlichst vermeiden sollte – aber auf Bibliotheken fremder haben Sie z.B. keinen Einfluss). Dann führt der Aufruf der Methode WriteLine() über Console.WriteLine() sowie mittels System.Console.WriteLine() bereits bei der Kompilierung zu einem Fehler, da Sie die Methode offensichtlich über die Variablen versuchen aufzurufen. Sie müssen dann den vollen Namen, angefangen von der Wurzel global angeben. Durch den ::-Operator wird global mit dem folgenden Namespace bzw. Member verbunden.

    global::System.Console.WriteLine("der ::-Operator");

    TIPP

    Sie sollten in der Regel Bezeichner so wählen, dass sie nicht mit den Namen des Namespaces System oder anderen Namespaces übereinstimmen. Andernfalls wird ihr Code schwerer verständlich und schlechter zu warten. Auch Fehler lassen sich dann mitunter schwerer finden.

    Der Zugriff über global wird in der Regel nur dann benötigt, wenn in einer neuen Version des .NET Frameworks neue Namespaces hinzugekommen sind, die dann eventuell zu Überschneidungen mit bereits existierenden Bezeichnern führen.

    Auch auf Aliase kann der ::-Operator angewandt werden. Für obiges Beispiel aus dem Abschnitt Aliase wäre auch der folgende Aufruf möglich:
    new MN::TestAusgabe().Ausgabe();

    INFO

    Der ::-Operator kann nicht für Aliase verwendet werden, die für einen Typ stehen, z.B.
    using MNC = MitNamespace.TestAusgabe;

    6.3 Die using-Anweisung

    Neben der using-Direktive zum Einbinden von Namespaces existiert außerdem die using-Anweisung die dazu dient, für ein Objekt einen begrenzten Gültigkeitsbereich zu schaffen. Dazu wird die using-Anweisung direkt im Code eingesetzt. In runden Klammern wird eine Instanz einer Klasse erzeugt. Diese ist nur innerhalb des folgenden Anweisungsblocks gültig. Der Typ des erzeugten Objekts muss die Schnittstelle IDisposable implementieren, da beim Verlassen des Gültigkeitsbereichs automatisch die Methode Dispose() aufgerufen wird (mehr dazu im Kapitel zu Interfaces). Implementiert der Typ diese Schnittstelle nicht, meldet bereits der Compiler einen Fehler.

    Beispiel: Begrenzte Gültigkeitsbereiche mit der using-Anweisung

    Direkt hinter der using-Anweisung wird in runden Klammern ein neues Objekt erzeugt, dessen Gültigkeitsbereich sich nur auf den folgenden Anweisungsblock der using-Anweisung beschränkt. Die Klasse TestObjekt implementiert das Interface IDisposable was im konkreten Fall heißt, dass sie eine Methode Dispose() mit der angegebenen Signatur besitzt. Beim Verlassen des Gültigkeitsbereichs der using-Anweisung wird automatisch die Methode Dispose() aufgerufen, die noch vor der Methode ReadLine(), also unmittelbar nach Verlassen des Blocks, ausgeführt wird. Damit stellen Sie von Ihrer Seite aus sicher, dass ein Objekt nur solange seine Ressourcen beansprucht wie notwendig. Diese Ressourcen sollten in der Methode Dispose() freigegeben werden.

    using System;
    namespace CSharpBuch.Kap06
    {
      public class BeschraenkteGueltigkeit
      {
        public static void Main(string[] args)
        {
          using(TestObjekt to = new TestObjekt())
          {
            to.Ausgabe();
          }
          Console.ReadLine();
        }
      }
    }
    public class TestObjekt : IDisposable
    {
      public void Ausgabe()
      {
        Console.WriteLine("Halli Hallo");
      }
      public void Dispose()
      {
        Console.WriteLine("Aufräumen mit Dispose");
      }
    }
    Listing 6.6: BeschraenkteGueltigkeit.cs

    6.4 Übungsaufgaben

    Aufgabe 1

    Erstellen Sie eine neue Konsolenanwendung. Definieren Sie einen dreistufig verschachtelten Namespace, der den Namen Ihrer Firma (oder den Familiennamen) und zwei weitere sinnvolle Untergliederungen enthält. Fügen Sie in die gleiche Datei einen weiteren Namespace mit einer beliebigen Klasse ein. Fügen Sie dieser Klasse eine Methode hinzu.

    Erzeugen Sie ein Objekt dieser Klasse. Verwenden Sie dazu den voll qualifizierten Namen der Klasse und rufen Sie die Methode auf.

    Aufgabe 2

    Erstellen Sie für die Klasse aus Aufgabe 1 einen Alias und verwenden Sie diesen, um ein Objekt dieser Klasse zu erzeugen.

    Aufgabe 3

    Verwenden Sie zum Erstellen des Klassenobjekts aus Aufgabe 1 die using-Anweisung. Rufen Sie im Anweisungsblock dieser Anweisung die Methode der Klasse auf.