Startseite  Index  <<  >>  

12 - Eigenschaften und Indexer

12.1 Eigenschaften

12.1.1 Einführung

Ziel der Kapselung von Daten in einer Klasse ist es, dass auf die Datenelemente nur über Methoden zugegriffen werden kann. Dadurch steht eine definierte Schnittstelle, nämlich die Set-Methode die den neuen Wert setzt, zur Verfügung, in der auch zentral nach Fehlern gesucht (debuggt) werden kann, da alle Änderungen der Daten durch diese Methode durchgeführt werden. Die lesende Methode, welche die Daten zurückliefert, kann diese Daten z.B. vor der Rückgabe noch formatieren oder in einen anderen Typ konvertieren.

Beispiel: Zugriff auf die Instanzvariablen über Methoden

Der Zugriff auf die private Variable name ist nur über die beiden Methoden GetName() und SetName() möglich. Sollte sich einmal ein "unerwarteter" Wert in der Variablen befinden, müssen Sie nur in der Methode SetName() "warten" (z.B. über einen Haltepunkt im Debugger), bis der fehlerhafte Wert als Parameter übergeben wird. Außerdem können Sie in der Set-Methode gleich den übergebenen Parameter validieren (überprüfen). In diesem Beispiel wird der Variablen name z.B. kein Wert zugewiesen, wenn der Methode SetName() eine leere Zeichenkette übergeben wird. Innerhalb der Get-Methode wird einfach der in der Variablen name gespeicherte Wert zurückgegeben. Allerdings wäre es auch möglich, diesen vorher noch entsprechend zu formatieren.

public class Person
{
  private string name;
  public void SetName(string name)
  {
    if(name != "")
      this.name = name;
  }
  public string GetName()
  {
    return name;
  }
}

Welche Nachteile hat diese Vorgehensweise?

  • Für den lesenden und schreibenden Zugriff auf die Variable werden verschiedene Methoden benötigt. Diesen Methoden können allerdings weitere Parameter übergeben werden. Durch die Verwendung von Eigenschaften ist dies später nicht möglich.
  • Wie soll der Wert einer solchen Variablen über das Eigenschaften-Fenster vom Visual Studio oder einer anderen grafischen Entwicklungsumgebung geändert werden?

    Die Definition einer Eigenschaft verbindet die Flexibilität und Sicherheit der Verwendung von Methoden mit der Einfachheit eines einheitlichen Zugriffs auf eine Variable über einen einzigen Bezeichner. Anstatt Methoden für den Zugriff auf eine Variable zu nutzen, wird ein Bezeichner definiert, der wie eine Variable verwendet wird. Wird diesem Bezeichner ein Wert zugewiesen, wird seine Set-Methode aktiviert. Umgekehrt wird beim Lesen die Get-Methode verwendet.

    INFO

    Eigenschaften können dadurch auch genutzt werden, um Datenbankverbindungen oder Dateien zu öffnen und zu schließen. Durch das Setzen einer Eigenschaft wie Active oder Enabled wird eine Methode aufgerufen, welche die Verbindung herstellt bzw. trennt. In der dahinter liegenden (optionalen) Variablen, die dann meist einen booleschen Typ besitzt, wird nur der aktuelle Zustand hinterlegt, z.B. true, für eine geöffnete Verbindung.

    Beispiel: Verwendung einer Eigenschaft

    Die Klasse Person verwaltet Informationen zu einer Person, in diesem Beispiel nur den Namen. Die Instanzvariable name, welche den Wert tatsächlich speichert, wurde als private deklariert, so dass sie nicht direkt manipuliert werden kann. Stattdessen wird eine Eigenschaft Name bereitgestellt. Diese liefert über einen so genannten get-Accessor den Wert der Variablen name zurück. Über einen so genannten set-Accessor wird der Wert der Variablen (nach vorheriger Prüfung des übergebenen Wertes) geschrieben. Auf den Wert, der in der Zeile p.Name = ... der Eigenschaft zugewiesen wird, kann im set-Accessor über das reservierte Wort value zugegriffen werden.

    using System;
    namespace CSharpBuch.Kap12
    {
      public class EinfacheEigenschaften
      {
        static void Main(string[] args)
        {
          Person p = new Person();
          p.Name = "Meier";
        }
      }
      public class Person
      {
        private string name;
        public string Name
        {
          get
          {
            return name;
          }
          set 
          { 
            if(value != "")
              name = value; 
          }
        }
      }
    }
    Listing 12.1: EinfacheEigenschaften.cs

    Syntax

  • Eine Eigenschaftsdeklaration wird mit der Angabe des Zugriffsmodifizierers eingeleitet. Es folgen der Datentyp und der Bezeichner der Eigenschaft.
  • Die Get- und Set-Methoden werden in geschweifte Klammern { und } eingeschlossen.
  • Die Anweisungen für den lesenden Zugriff auf die Eigenschaft werden mit dem Schlüsselwort get eingeleitet und in geschweifte Klammern eingeschlossen. Wie in einer Methode ist die Angabe von return mit einem Rückgabewert entsprechend des Typs der Eigenschaft notwendig.
  • Die Methode zum Schreiben des Eigenschaftswertes wird mit set eingeleitet. Auf den zugewiesenen Wert wird über das reservierte Schlüsselwort value zugegriffen.
  • Die reservierten Wörter get und set werden auch als get- und set-Accessoren bezeichnet.
  • Je nachdem, ob Sie die Definition von get oder set weglassen, entsteht eine nicht lesbare bzw. eine schreibgeschützte Eigenschaft.
  • Eigenschaften können nicht per ref oder out an Methoden übergeben werden, da es sich ja eigentlich um Methoden handelt.
  • Sie können statische und virtuelle Eigenschaften deklarieren. Eigenschaften lassen sich außerdem mit new überschreiben.
  • Auch die Verwendung von sealed oder abstract ist in Zusammenhang mit Eigenschaften möglich.

    INFO

    Eine Eigenschaft wird mit einem Bezeichner in Pascalschreibweise deklariert. Wird eine Variable benötigt, die den aktuellen Wert der Eigenschaft repräsentiert, wird diese in der Regel als private und in Kamelschreibweise, also beginnend mit einem Kleinbuchstaben, mit dem ansonsten gleichen Bezeichnernamen deklariert.

    12.1.2 Automatisch implementierte Eigenschaften

    Viele Eigenschaften besitzen einen recht einfachen Aufbau, da lediglich eine Variable zur Aufnahme des Wertes, eine Get-Methode zum Zurückliefern des Wertes und eine Set-Methode zum Setzen des Wertes benötigt werden.
    private  name;
    public  Name
    {
      get { return name; } 
      set { name = value; }
    }

    Diese einfache Logik kann auch der Compiler generieren. Dazu wird vom Compiler eine private, anonyme Variable zur Aufnahme des Wertes bereitgestellt, die gelesen und geschrieben wird. Der direkte Zugriff auf diese Variable ist nicht möglich. Die Get- und Set-Methoden sind automatisch öffentlich. Einzige Ausnahme ist, dass Sie vor der Set-Methode den Modifizierer private setzen können, um eine schreibgeschützte Eigenschaft zu erhalten. Das Weglassen der Get- oder der Set-Methode ist nicht möglich.

    public  Name { get; set; }
    public  Name { get; private set; }

    12.1.3 Asymmetrischer Accessor-Zugriff

    Bis zum .NET Framework 1.1 konnten für die get- und set-Accessoren keine Zugriffsmodifizierer angegeben werden. Der Zugriff wurde über die Angabe des Modifizierers an der Eigenschaft bestimmt. Seit der Version 2.0 können die Modifizierer auch direkt vor get und set angegeben werden, und zwar mit unterschiedlichen Rechten. In der Regel wird dies zur Definition von Einschränkungen beim set-Accessor genutzt, den dann nur abgeleitete Klassen verwenden können.

    Beispiel: Unterschiedliche Zugriffsmodifizierer bei Eigenschaften nutzen

    Der Schreibzugriff auf die Eigenschaft Name wird nur noch abgeleiteten Klassen erlaubt.

    public string Name
    {
      get 
      {
        return name;
      }
      protected set 
      { 
        name = value; 
      }
    }

    Syntax

  • Geben Sie vor den Accessoren get oder set einen Zugriffsmodifizierer an.
  • Die Angabe eines Zugriffsmodifizierers ist nur bei get oder bei set erlaubt. Der jeweils andere Accessor verwendet die Angabe bei der Deklaration der Eigenschaft. Außerdem können Sie einen Modifizierer an einem Accessor nur dann angeben, wenn beide Accessoren verwendet werden.
  • Der an get oder set verwendete Zugriffsmodifizierer muss die Zugriffsrechte stärker einschränken als der Modifizierer in der Deklaration der Eigenschaft.
  • Die Angabe eines Modifizierers ist nicht bei der Deklaration von Eigenschaften in Schnittstellen erlaubt. Gibt die Schnittstelle allerdings entweder nur einen get- oder nur einen set-Accessor vor, kann der jeweils andere Accessor in der Implementierung mit einem anderen Modifizierer definiert werden.

    12.1.4 Eigenschaften in Schnittstellen verwenden

    Um sicherzustellen, dass ein Typ eine bestimmte Eigenschaft besitzt, können Eigenschaften auch in Schnittstellen deklariert und später von Klassen implementiert werden. Bei der Deklaration in der Schnittstelle wird nach get oder set lediglich ein Semikolon gesetzt. Der Rest der Eigenschaftsdeklaration bleibt gleich. Sie können in einer Schnittstelle auch nur die get- oder set-Accessoren vorgeben. Eine Klasse, welche die Schnittstelle implementiert, kann dann den anderen Accessor ebenfalls zur Verfügung stellen. Für die Schnittstellenimplementierung wäre es aber nicht notwendig.

    Beispiel: Eigenschaften als Bestandteil der Schnittstelle eines Typs verwenden

    Die Schnittstelle IKlassenInfo deklariert eine Eigenschaft Version. Klassen, welche die Schnittstelle implementieren, geben darüber ihre interne Versionsnummer zurück. Das Datenelement, welches die Version tatsächlich speichert, wird in der Klasse Konto mit dem Namen version bereitgestellt. Außerdem enthält die Klasse eine statische Eigenschaft Autor, um unabhängig von einem Objekt den Autor (bzw. den Entwickler) einer Klasse zu ermitteln.

    using System;
    namespace CSharpBuch.Kap12
    {
      public class EigenschaftenInSchnittstellen
      {
        static void Main(string[] args)
        {
          Konto ie = new Konto();
          Konto.Autor = "Dirk F.";
          ie.Version = "0.01";
          Console.WriteLine("Autor: " + Konto.Autor);
          Console.WriteLine("Version: " + ie.Version);
        }
      }
      public interface IKlassenInfo
      {
        string Version
        {
          get;
          set;
        }
      }
      public class Konto: IKlassenInfo
      {
        private string version;
        private static string autor;
        public string Version
        {
          get
          {
            return version;
          }
          set
          {
            version = value;
          }
        }
        public static string Autor
        {
          get
          {
            return autor;
          }
          set
          {
            autor = value;
          }
        }
      }
    }

    Listing 12.2: EigenschaftenInSchnittstellen.cs
  • 12.2 Indexer

    12.2.1 Einführung

    Ein erlaubt es, eine Klasse unter anderem wie ein Array zu nutzen. Dazu wird einem Objekt vom Typ der Klasse in eckigen Klammern ein Index übergeben. Als Ergebnis wird ein Wert zurückgegeben, der über den (Index)Parameter identifiziert wird. Diese Vorgehensweise ist dann sinnvoll, wenn eine Klasse eine Sammlung von Objekten oder Daten verwaltet und ein einfacher Zugriff darauf bereitgestellt werden soll.

    Wenn beispielsweise eine Klasse eine Menge von Maschinenteilen in einem Array verwaltet, könnte über einen Indexer der Zugriff auf diese Teile hergestellt werden. Häufig werden Indexer in Verbindung mit den Auflistungsklassen (Collections) verwendet, um auf ein bestimmtes Element einer Datenstruktur zuzugreifen.

    Aber es gibt auch andere Anwendungsmöglichkeiten. Eine Klasse muss die Daten, die über den Index angesprochen werden, nicht einmal selbst verwalten. Sie kann bei Übergabe eines Index auch in einer Datenbanktabelle den durch den Index angegebenen Datensatz auslesen und dessen Werte über ein Objekt eines passenden Typs zurückgeben.

    Beispiel: Indexer für eine Klasse bereitstellen

    Die Klasse Daten besitzt ein Array datenFeld, in dem Strings verwaltet werden. Für dieses Feld werden über einen Indexer ein Lese- und ein Schreibzugriff bereitgestellt. Der Indexer wird durch Angabe des Zugriffsmodifizierers, des Rückgabetyps und des Schlüsselworts this deklariert. Er besitzt also keinen eigenen Namen, sondern wird über ein Objekt der Klasse angesprochen. In eckigen Klammern wird der Parameter des Indexers angegeben, der dann in den get- und set-Accessoren zum Auffinden des passenden Elements genutzt werden kann.

    public class Daten
    {
      string[] datenFeld = { "", "", "" };
      public string this[int index]
      {
        get
        {
          return datenFeld[index];
        }
        set
        {
          datenFeld[index] = value;
        }
      }
    }

    Syntax

  • Ein Indexer wird grundsätzlich wie eine Eigenschaft deklariert, d.h., er besitzt einen Zugriffsmodifizierer, einen Rückgabetyp sowie einen get- und/oder einen set-Accessor.
  • Als Bezeichner für den Indexer wird allerdings das Schlüsselwort this verwendet. Damit kann der Indexer über ein Objekt der Klasse aufgerufen werden.
  • Hinter das Schlüsselwort this wird in eckigen Klammern der Typ und ein Bezeichner für den Index angegeben. Über den Index kann dann das gewünschte Datenelement identifiziert werden.
  • Der Rückgabetyp sowie der Typ des Indexparameters sind frei definierbar.
  • Indexer können überladen werden, d.h., Sie können Indexer mit unterschiedlichen Parametern deklarieren. Sie können auch mehrere Parameter verwenden, z.B. public string this[int zeile, int spalte], um beispielsweise einen Wert aus einer Tabelle zurückzuliefern.
  • Wird der set-Accessor verwendet, greifen Sie auf den dem Indexer zugewiesenen Wert über das Schlüsselwort value zu.
  • Die durch einen Indexer zurückgegebenen Werte können nicht als ref- oder out-Parameter an Methoden übergeben werden.
  • Ein Indexer wird wie ein Array verwendet. Geben Sie hinter dem Objektnamen in eckigen Klammern die Parameter an, mehrere mit Komma getrennt.

    INFO

    In der API-Dokumentation verbirgt sich ein Indexer unter einer Eigenschaft mit dem Namen Item. Diese Eigenschaft existiert nicht tatsächlich, wenn Sie mit C# programmieren, da C# Indexer direkt unterstützt. Die Eigenschaft ist nur dann über diesen Namen verfügbar, wenn eine .NET-Programmiersprache keine Indexer unterstützt. Über das Attribut IndexerName könnten Sie einen anderen Standardnamen für den Indexer definieren.

    Beispiel: Klassenelemente über Indexer durchlaufen

    Die Klasse Lottozahlen dient zur Simulierung einer Lottoziehung. Dazu werden im Konstruktor der Klasse die Anzahl der benötigten Zahlen und der Maximalwert übergeben. Über die Methode ZiehungDurchfuehren() werden die benötigten Zufallszahlen erzeugt (ohne Rücksicht auf doppelt gezogene Zahlen). Damit die "berechneten" Zahlen einfach bereitgestellt werden können, wird ein Indexer deklariert. Dieser erhält als Parameter den Null-basierten Index für das Array ziehung mit den gezogenen Zahlen. Im get-Accessor wird außerdem eine minimale Überprüfung des Indexwertes vorgenommen.

    using System;
    namespace CSharpBuch.Kap12
    {
      public class KlassenIndizieren
      {
        static void Main(string[] args)
        {
          Lottozahlen lz = new Lottozahlen(5, 35);
          lz.ZiehungDurchfuehren();
          for(int zahl = 0; zahl < 5; zahl++)
            Console.WriteLine("Gezogen wurde: {0}", lz[zahl]);
        }
      }
      public class Lottozahlen
      {
        private int[] ziehung;
        private int anzahl;
        private int maxWert;
        public Lottozahlen(int anzahl, int maxWert)
        {
          this.maxWert = maxWert;
          this.anzahl = anzahl;
        }
        public int this[int index]
        {
          get
          {
            if((index >= 0) & (index < anzahl))
              return ziehung[index];
            else
              return 0;
          }
        }
        public void ZiehungDurchfuehren()
        {
          Random rd = new Random(DateTime.Now.Millisecond);
          ziehung = new int[anzahl];
    
          for(int i = 0; i < anzahl; i++ )
            ziehung[i] = rd.Next(maxWert) + 1;
        }
      }
    }
    Listing 12.3: KlassenIndizieren.cs

    INFO

    Das hier gezeigte Beispiel führt nur rudimentäre Prüfungen der Index- und sonstigen Parameterwerte durch. Stellen Sie bei Verwendung eines Indexers sicher, dass die Indexwerte auch verfügbar sind. Implementieren Sie gegebenenfalls eine Eigenschaft Count (oder Anzahl), welche die Anzahl der über den Index verfügbaren Elemente zurückgibt.

    12.2.2 Indexer in Schnittstellen verwenden

    Ebenso wie Eigenschaften können Sie Indexer in Schnittstellen deklarieren. Es werden genau wie bei Eigenschaften die get- und set-Accessoren nicht implementiert, die restliche Deklaration des Indexers bleibt gleich. Sie dürfen allerdings keinen Zugriffsmodifizierer angeben.

    Beispiel: Ein Interface zur Lottoziehung deklarieren

    Das Interface ILotto erlaubt den lesenden und schreibenden Zugriff auf die erzeugten Lottozahlen. Über den schreibenden Zugriff könnten Sie eventuell später etwas nachhelfen, wenn Sie nur zwei Richtige haben.
    public interface ILotto
    {
      int this[int index]
      {
        get;
        set;
      }
      void ZiehungDurchfuehren();    
    }

    Vergleich von Eigenschaften und Indexer

    Die folgende Tabelle fasst die Gemeinsamkeiten und Unterschiede zwischen Eigenschaften und Indexern zusammen und stellt mögliche Einsatzgebiete vor.
    EigenschaftenIndexer
    Beide werden wie Variablen verwendet. Zur Wertzuweisung bzw. -rückgabe werden aber immer Methoden verwendet. Es ist möglich, nur einen lesenden oder schreibenden Zugriff bereitzustellen.
    Beide dürfen nicht per ref oder out an Methoden übergeben werden.
    Der Zugriff auf eine Eigenschaft erfolgt über einen Namen.Der Zugriff auf den Indexer erfolgt wie bei einem Array (eckige Klammern) bzw. einer Methode (ein bis mehrere Indexparameter werden durch Komma getrennt übergeben).
    Eigenschaften können statisch wie auch innerhalb der Objektinstanz deklariert werden.Indexer sind immer mit einer Instanz verbunden.
    Als Parameter wird nur der neue Wert der Eigenschaft im set-Accessor übergeben.In einem Indexer erhalten die get- und set-Accessoren die in eckigen Klammern übergebenen Parameter. Der set-Accessor erhält bei Zuweisungen an den Indexer noch den übergebenen Wert über das Schlüsselwort value.
    Verwenden Sie Eigenschaften, um einen über Methoden abgesicherten lesenden und schreibenden Zugriff auf Datenmember durchzuführen. Kapseln Sie komplexe Operationen wie das Herstellen einer Datenbankverbindung in einer Eigenschaft, um die Programmierung zu vereinfachen.Verwenden Sie Indexer, wenn Sie einen einfachen, indexbasierten Zugriff auf die durch eine Klasse verwalteten Datenelemente bereitstellen wollen. Diese Vorgehensweise wird sehr häufig in den Auflistungsklassen verwendet (sieht Kapitel "Auflistungen").
    Tabelle 12.1: Unterschiede und Gemeinsamkeiten von Eigenschaften und Indexern

    12.3 Übungsaufgaben

    Aufgabe 1

    Erstellen Sie eine Klasse Zufallszahl, die eine Eigenschaft Zahl besitzt. Diese Eigenschaft soll nur lesbar sein und bei jedem Lesezugriff eine Zufallszahl im Bereich von 1 bis 100 liefern.

    Aufgabe 2

    Deklarieren Sie eine Schnittstelle, welche die Eigenschaften Tag, Monat und Jahr definiert. Implementieren Sie die Schnittstelle in einer Klasse Datum. Überprüfen Sie beim Setzen der Eigenschaften, dass die übergebenen Werte plausibel sind.

    Aufgabe 3

    Eine Klasse MeineAutos verwaltet über ein Array aus 5 Elementen vom Typ Auto Daten Ihrer vergangenen (oder zukünftigen) PKWs. Die dazu benötigte Klasse Auto sollte einige sinnvolle Eigenschaften besitzen.

    Über eine Eigenschaft Count liefert die Klasse die Anzahl der verwalteten Autos zurück (es müssen ja nicht wie hier in der Übung immer genau 5 Autos sein). Die Klasse stellt einen Indexer zur Verfügung, um auf die einzelnen Auto-Objekte zugreifen zu können. Testen Sie die Funktionsweise.