Beratung Veröffentlichungen Beratung Programmierung Server Seminare Lösungen Unternehmen Kontakt Start
 

Comelio-Blog > C#.NET > Indexer

Indexer-Einsatz in C#.NET

Die Indexer-Technik von C#.NET zählt nicht zu den elementaren Bausteinen von objektorientierten Programmiersprachen und bietet in erster Linie für den Klienten von Klassen, die mit einem solchen Indexer ausgestattet sind, eine vereinfachte Verwendung beim Lesen und Schreiben. Sinn und Zweck eines Indexers besteht darin, es Klienten zu ermöglichen, auf ein Objekt mit der gleichen Index-Schreibweise in eckigen Klammern zuzugreifen wie auf Arrays. Dies vereinfacht den Quelltext für den Klienten und ermöglicht kürzere Ausdrücke und Formulierungen. In der einfachsten Variante organisiert man den Indexer mit Hilfe eines Ganzzahlwertes, wie es auch für eine einfache Array-Klasse der Fall ist. Dieser Artikel zeigt, wie sich Indexer für C#.NET verwenden lassen.

Kontakt

Anrede* Herr Frau
Vorname*
Nachname*
Firma
E-Mail*
Tel-Nr.
Bereich*
Freitext

C#.NET - Indexer

Die Indexer-Technik zählt nicht zu den elementaren Bausteinen von objektorientierten Programmiersprachen und bietet in erster Linie für den Klienten von Klassen, die mit einem solchen Indexer ausgestattet sind, eine vereinfachte Verwendung beim Lesen und Schreiben. Für die Bereitstellung ergibt sich allerhand besondere Syntax, die wir in diesem Artikel vorstellen..

Allgemeine Funktionsweise

Sinn und Zweck eines Indexers besteht darin, es Klienten zu ermöglichen, auf ein Objekt mit der gleichen Index-Schreibweise in eckigen Klammern zuzugreifen wie auf Arrays. Dies vereinfacht den Quelltext für den Klienten und ermöglicht kürzere Ausdrücke und Formulierungen. In der einfachsten Variante organisiert man den Indexer mit Hilfe eines Ganzzahlwertes, wie es auch für eine einfache Array-Klasse der Fall ist. Dabei wird in einer öffentlichen (dieses Mal nicht statischen, da zum Objekt gehörenden) Eigenschaft das Schlüsselwort this verwendet und der Indexparameter angegeben. Diese Eigenschaft besitzt dann ebenfalls einen get- und set-Block, in dem angegeben wird, was denn beim Setzen und Lesen von neuen Werten im Objekt geschehen soll. Die Verwaltung der Werte läuft letztendlich immer darauf hinaus, dass ein privates Feld in Form eines Arrays, ArrayList oder Collection vorgehalten wird, in dem Elemente abgelegt und aus dem Elemente gelesen werden.

Die allgemeine Syntax für diesen Fall hat die nachfolgende Form:

public int this [int index]  {
      get    { return Array-Feld[index];  }
      set    {  Array-Feld[index] = value;  }
   }
Da die Vorteile sich erst im Klientenquelltext ergeben, beginnen wir zunächst mit seiner Vorstellung und Diskussion. Es werden zwei Punkt-Objekte erstellt, die als Menge gespeichert, verschoben und bearbeitet werden sollen. Dies soll in Form einer Gerade geschehen, wobei dies allerdings in diesem Fall keine eigene Klasse sein wird.

  1. Die erste Möglichkeit erstellt mit Hilfe von Punkt[] gerade1 = new Punkt[2] {a, b}; ein neues Array vom Typ Punkt und damit eine Gerade, auf der nun Punkte liegen. Sollten sie nicht tatsächlich auf einer Gerade liegen, sondern mehr einer Punktwolke sein, könnte man die Variable umbenennen oder mit der Regressionsformel die angenäherte Geradensteigung verwenden. Doch um diese Feinheiten soll es jetzt hier nicht gehen.
  2. Die zweite Möglichkeit spart sich den Aufruf eines eigenen Arrays und erstellt mit dem zweiten Konstruktor einfach einen leeren Punkt Punkt gerade2 = new Punkt();, in dem nun mehrere Punkte gespeichert werden sollen. Der Name soll hier deutlich machen, dass kein einzelner Punkt, sondern eine Punktmenge bzw. eine Gerade erstellt wird. Durch die Verwendung eines Indexers hat man nun die Gelegenheit, neue Punkte über eine Array-Schreibweise wie gerade2[0] = a; zu setzen, die in der ersten Möglichkeit beim Schreiben und Lesen wie in gerade1[1].ToString(); ebenfalls zulässig ist. Dort handelte es sich allerdings tatsächlich um ein Array, im zweiten Fall dagegen tritt ein Indexer hinzu.

// Punkte erstellen
Punkt a = new Punkt(1, 2);
Punkt b = new Punkt(4, 8);
// 1. Möglichkeit: Punkte in einem Array speichern
Punkt[] gerade1 = new Punkt[2] {a, b};
ausgabe.Text += gerade1[1].ToString();
// 2. Möglichkeit: Indexer verwenden
Punkt gerade2 = new Punkt();
gerade2[0] = a;
gerade2[1] = b;
ausgabe.Text += gerade2[1].ToString();
Verwenden eines Indexers

Wie sieht nun die Voraussetzung in der Klasse für diese raffinierte Technik aus? Zunächst einmal bleiben die Koordinaten und auch der Konstruktor, in denen beide gesetzt werden, erhalten. Dann ergänzen wir dies um einen weiteren Konstruktor ohne Parameter, der eigentlich nur dann eingesetzt werden sollte, wenn mehrere Punkte gespeichert werden sollen. Ansonsten müsste man im vorherigen Konstruktor die entsprechenden Werte ebenfalls in der ArrayList punkte als neuen Punkt speichern. Nach dieser nicht ganz perfekten Quelltextstelle folgt der Indexer, der in unserem Beispiel ein Objekt vom Typ Punkt zurückliefert, sodass man diesen Datentyp auch in value findet und daher darauf angewiesen ist, dass nicht etwa zwei Koordinaten übergeben werden, sondern sofort fertige Punkte mit new Punkt(int x, int y).

public class Punkt {
    public int x, y;
    private ArrayList punkte = new ArrayList();
    public Punkt() { }
    public Punkt(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public Punkt this[int index] {
        get {
            return (Punkt)punkte[index];
        }
        set {
            punkte.Add(value);
        }
    }
    public override string ToString() {
        return "(" + x + "/" + y + ")";
    }
}
Erstellen eines Indexers

Man erhält tatsächlich in der Ausgabe zweimal das Ergebnis (4/8).

Die Abbildung zeigt noch einmal, wie zwei verschiedene Punkte mit den Koordinaten 1/2 und 4/8 sowie mit Hilfe des zweiten parameterlosen Konstruktors eine Variable namens gerade vom gleichen Typ erzeugt werden. Damit diese ihrem Namen gerecht wird, lassen sich nun beide Punkte in ihr speichern, wobei der Indexer zum Einsatz kommt, welcher die in Array-Schreibweise übergebenen Punkte in der ArrayList punkte speichert, die in der Klasse Punkt verwaltet wird.

Verwendung eines Indexers

Verwendung eines Indexers

Datentypen anderer Klassen

Nun hat das vorherige Beispiel schon gezeigt, dass es möglicherweise semantisch nicht sonderlich sinnvoll ist, einen Datentyp sowohl für ein einzelnes Exemplar als auch für eine Gruppe zu erstellen. Insbesondere in diesem Fall, wo eine Gerade ohnehin nur existieren kann, wenn Punkte vorhanden sind, ist es vermutlich besser, diese beiden Phänomene zu trennen. Interessant ist dieses Beispiel natürlich auch für das Thema der Kompositionsbeziehung, die allerdings hier nicht vorgestellt werden soll.

Zunächst erstellen wir also eine Klasse Gerade, in der die gerade schon verwendete ArrayList für die Speicherung von mehreren Objekten des Typs Punkt vorhanden ist. Eine Gerade lässt sich hier nun nur erstellen, in dem wenigstens zwei Punkte im Konstruktor übergeben werden, die dann der Punktsammlung hinzugefügt werden. Neu an diesem Beispiel ist nun, dass es zwei Klassen gibt und dass der Indexer der Klasse Gerade Objekte vom Typ Punkt, also Objekte vom Typ einer anderen Klasse, verarbeitet.

public class Gerade {
    private ArrayList punkte = new ArrayList();
    public double Steigung   { /*...*/  }
    public Gerade(Punkt a, Punkt b)   {
        punkte.Add(a);
        punkte.Add(b);
    }
    public void RemovePunkt(Punkt punkt)   { /*...*/  }
    public void RemovePunkt(int index) { /*...*/  }
    public Punkt this[int index]  {
        get  { return (Punkt)punkte[index];  }
        set  { punkte.Add(value);  }
    }
}
Klasse mit Indexer für Typ Punkt

Im Klientenquelltext sieht die gesamte Konstruktion wiederum wesentlich einfacher aus als in der Klassenkosntruktion, weil hier erst die syntaktische Verkürzung und Vereinfachung zum Tragen kommt. Zunächst erstellen wir eine konkrete Gerade durch die Übergabe von zwei Punkt-Objekten und testen die Funktionsweise, indem die Steigung ausgegeben wird, deren Berechnung im vorherigen Quelltext aus Platzgründen ausgelassen wurde. Dann fügen wir einen neuen Punkt hinzu, wobei jetzt die sehr kurze Array-Schreibweise zum Einsatz kommt. Nach einem erneuten Test, ob die Steigung nun korrekt über die Formel zur Berechnung der Regressionsgeradensteigung ermittelt wird, entfernen wir einen Punkt klassisch über die RemovePunkt-Methode, die weiterhin wie ihr überladenes Pendant einsetzbar ist. Zum Schluss lesen wir über die Array-Schreibweise einen Punkt aus und benutzen dies als Initialisierung für das neu erzeugte Punkt-Objekt.

// Traditionelle Gerade erstellen
Gerade gerade = new Gerade(new Punkt(1, 2), 
                           new Punkt(4, 8));
ausgabe.Text += gerade.Steigung + " / ";
// Indexer verwenden
gerade[2] = new Punkt(2, 3);
ausgabe.Text += Math.Round(gerade.Steigung, 2) + " / ";
// Bearbeiten über Methoden
gerade.RemovePunkt(2);
// Punkt lesen
Punkt c = gerade[1];
ausgabe.Text += Math.Round(gerade.Steigung, 2)
                + " Punkt c: " + c.ToString();
Verwendung einer Klasse mit Indexer für Typ Punkt

Man erhält insgesamt die Ausgabe 2 / 2,07 / 2 Punkt c: (4/8).

Die Abbildung zeigt das UML-Diagramm für die beiden Klassen, wobei insbesondere die Aggregationsbeziehung von Bedeutung ist, die über die verschiedenen typischen Verwaltungsmethoden und den Konstruktor, aber auch in diesem Fall über den Indexer umgesetzt wird.

Klassendiagramm von Gerade und Punkt

Klassendiagramm von Gerade und Punkt

    Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer Comelio GmbH C#.NET - Indexer