7.4 Indexer 

In Kapitel 2, »Grundlagen der Sprache C#«, haben Sie gelernt, mit Arrays zu arbeiten. Sie wissen, wie Sie ein Array deklarieren und auf die einzelnen Elemente zugreifen können, zum Beispiel:
int[] intArr = new int[10]; intArr[3] = 125;
Mit C# können Sie Klassen und Strukturen so definieren, dass deren Objekte wie ein Array indiziert werden können. Indizierbare Objekte sind in der Regel Objekte, die als Container für andere Objekte dienen – vergleichbar einem Array. Das .NET Framework stellt uns eine ganze Reihe solcher Klassen zur Verfügung, die als Collections oder Auflistungen bezeichnet werden. Diese agieren ähnlich den uns schon bekannten Arrays, verwalten also Objekte.
Stellen Sie sich vor, Sie würden die Klasse Fußballmannschaft entwickeln. Eine Mannschaft setzt sich aus vielen Einzelspielern zusammen, die innerhalb der Klasse in einem Array vom Typ Spieler verwaltet werden. Wenn Sie die Klasse Fußballmannschaft mit
Fußballmannschaft Wacker = new Fußballmannschaft();
instanziieren, wäre es doch zweckdienlich, sich von einem bestimmten Spieler mit der Anweisung
string name = Wacker[2].Zuname;
den Zunamen zu besorgen. Genau das leistet ein Indexer. Wir übergeben dem Objekt einen Index in eckigen Klammern, der ausgewertet wird und die Referenz auf ein Spieler-Objekt zurückliefert. Darauf können wir mit dem Punktoperator den Zunamen des gewünschten Spielers ermitteln, vorausgesetzt, diese Eigenschaft ist in der Klasse Spieler implementiert.
Ein Indexer ist prinzipiell eine Eigenschaft, die mit this bezeichnet wird und in eckigen Klammern den Typ des Index definiert. Weil sich this immer auf ein konkretes Objekt bezieht, können Indexer niemals als static deklariert werden. Die Definition lautet:
<Modifikatoren> <Datentyp> this[<Parameterliste>]
Als Modifizierer sind neben den Zugriffsmodifikatoren auch new, virtual, sealed, override und abstract zulässig. Wenn Sie sich in Erinnerung rufen, was Sie im vorhergehenden Abschnitt über Operatorüberladung gelernt haben, wird klar, dass Indexer eine Überladung des []-Operators sind.
Wenn eine Klasse einen Indexer definiert, darf diese Klasse keine Item-Methode haben, weil interessanterweise ein Indexer als Item-Methode interpretiert wird.
Mit diesem Wissen ausgestattet, sollten wir uns nun die Implementierung der Klasse Fußballmannschaft ansehen.
// -------------------------------------------------------------- // Beispiel: ...\Kapitel 7\Indexer // -------------------------------------------------------------- class Program { static void Main(string[] args) { Fußballmannschaft Wacker = new Fußballmannschaft(); // Spieler im Team aufnehmen Wacker[0] = new Spieler("Fischer", 23); Wacker[1] = new Spieler("Müller", 19); Wacker[2] = new Spieler("Mamic", 33); Wacker[1] = new Spieler("Meier", 31); // Spielerliste ausgeben for (int index = 0; index < 25; index++) { if (Wacker[index] != null) Console.WriteLine("Name: {0,-10}Alter: {1}", Wacker[index].Zuname, Wacker[index].Alter); } Console.ReadLine(); } } // Fussballmannschaft public class Fußballmannschaft { private Spieler[] team = new Spieler[25]; // Indexer public Spieler this[int index] { get { return team[index]; } set { // prüfen, ob der Index schon belegt ist if (team[index] == null) team[index] = value; else // nächsten freien Index suchen for (int i = 0; i < 25; i++) { if (team[i] == null) { team[i] = value; return; } } } } } // Spieler public class Spieler { public string Zuname; public int Alter; public Spieler(string zuname, int alter) { Zuname = zuname; Alter = alter; } }
Jede Instanz der Klasse Fußballmannschaft verhält sich wie ein Array. Dafür verantwortlich ist der Indexer, der über das Schlüsselwort this deklariert wird und einen Integer entgegennimmt. Der Indexer ist vom Typ Spieler. Der lesende und schreibende Zugriff auf ein Element erfolgt unter Angabe seines Index, also beispielsweise:
Wacker[6];
Die interne Struktur eines Indexers gleicht der einer Eigenschaftsmethode: Sie enthält einen get- und einen set-Accessor. get wird aufgerufen, wenn durch die Übergabe des int-Parameters Letzterer als Index der Spieler-Arrays ausgewertet wird und den entsprechenden Spieler aus dem privaten Array zurückgibt. Die Zuweisung eines weiteren Spielers hat den Aufruf des set-Zweiges zur Folge. Dabei wird überprüft, ob der angegebene Index noch frei oder bereits belegt ist. Im letzteren Fall wird der erste freie Index gesucht.
7.4.1 Überladen von Indexern 

In einem herkömmlichen Array erfolgt der Zugriff auf ein Element grundsätzlich über den Index vom Typ int, aber Indexer lassen auch andere Datentypen zu. In vielen Situationen ist es sinnvoll, anstelle des Index eine Zeichenfolge anzugeben, mit der ein Element identifiziert wird. Meistens handelt es sich dabei um den Namen des Elements. Sind mehrere unterschiedliche Zugriffe wünschenswert, können Indexer nach den bekannten Regeln hinsichtlich Anzahl und Typ der Parameter überladen werden.
Das folgende Beispiel zeigt eine Indexerüberladung. Dazu benutzen wir das Beispiel aus dem vorherigen Abschnitt und ergänzen die Klasse Fußballmannschaft um einen weiteren Indexer in der Weise, dass wir auch über dem Namen des Spielers auf das zugehörige Objekt zugreifen können, also zum Beispiel mit
Spieler spieler = Wacker["Fischer"];
Angemerkt sei dabei, dass das Beispiel nur so lange wunschgemäß funktioniert, solange die Namen eindeutig sind. Sollten mehrere Spieler gleichen Namens in der Liste zu finden sein, müssten weitere Kriterien zur eindeutigen Objektbestimmung herangezogen werden. Das soll aber an dieser Stelle nicht das Thema sein.
// -------------------------------------------------------- // Beispiel: ...\Kapitel 7\IndexerÜberladung // -------------------------------------------------------- class Program { static void Main(string[] args) { Fußballmannschaft Wacker = new Fußballmannschaft(); // Spieler im Team aufnehmen Wacker[0] = new Spieler("Fischer", 23); Wacker[1] = new Spieler("Müller", 19); Wacker[2] = new Spieler("Mamic", 33); Wacker[1] = new Spieler("Meier", 31); Console.Write("Spieler suchen: ... "); string spieler = Console.ReadLine(); if (Wacker[spieler] != null) Console.WriteLine(Wacker[spieler].Alter); else Console.WriteLine("Der Spieler gehört nicht zum Team."); Console.ReadLine(); } } // Fußballmannschaft public class Fußballmannschaft { private Spieler[] team = new Spieler[25]; // Indexer public Spieler this[int index] { ... } public Spieler this[string name] { get { for (int index = 0; index < 25; index++) { if (team[index] != null && team[index].Zuname == name) return team[index]; } return null; } } }
Die Überladung des Indexers mit einem string enthält nur den get-Accessor, da die Zuweisung eines neuen Spieler-Objekts nur anhand seines Namens in diesem Beispiel unsinnig wäre. Im get-Accessor wird eine Schleife über alle Indizes durchlaufen. Jeder Index wird dahingehend geprüft, ob er einen von null abweichenden Inhalt hat. Ist der Inhalt nicht null und verbirgt sich hinter dem Index auch das Spieler-Objekt mit dem gesuchten Namen, wird das Objekt an den Aufrufer zurückgegeben. Diese Operation wird durch
if (team[index] != null && team[index].Zuname == name) return team[index];
beschrieben. Sollte sich ein Spieler mit dem gesuchten Namen nicht in der Mannschaft befinden, ist der Rückgabewert null.
7.4.2 Parameterbehaftete Eigenschaften 

Eigenschaften sind von Haus aus parameterlos. Mit anderen Worten: Sie können einen Eigenschaftswert nicht in Abhängigkeit von einer oder mehreren Nebenbedingungen setzen. Dieses Manko lässt sich mit Indexern beheben, sodass beispielsweise mit
myObject.TheProperty[2] = 10;
der Eigenschaftswert festgelegt werden kann.
Parametrisierte Eigenschaften sind von Bedeutung, wenn Randbedingungen den von der Eigenschaft beschriebenen Wert beeinflussen. In der fiktiven Eigenschaft TheProperty lautet die Randbedingung »2«. Unter dieser Prämisse soll der Eigenschaft die Zahl 10 zugewiesen werden. Der Code ähnelt ohne Zweifel einem Array und lässt sich auch so interpretieren: Es handelt sich um eine indizierte Sammlung gleichnamiger Eigenschaftselemente.
Wir wollen uns jetzt ansehen, wie in einer Klasse eine parameterbehaftete Eigenschaft realisiert werden kann. Dazu stellen wir uns eine Klasse Car mit einer Eigenschaft Color vor. Ein Car-Objekt beschreibt das Auto eines beliebigen Herstellers. Wir wissen alle, dass die verschiedenen Autoproduzenten unterschiedliche Farbpaletten anbieten – oft modellabhängig. Einen Ferrari gibt es möglicherweise nur in Rot, Gelb oder Schwarz; kaufen Sie einen häufiger vertretenen Typ, können Sie vielleicht unter 20 verschiedenen Farben auswählen. Diese Situation soll die Eigenschaft Color der Car-Klasse beschreiben.
Der Eigenschaft Color wollen wir als Argument eine Zeichenfolge übergeben, die den Hersteller beschreibt. Zurückgeliefert wird daraufhin die Palette der zur Auswahl stehenden Farben. Ein Aufruf könnte dann wie folgt aussehen:
int[] colors = myCar.Color["Mazda"];
Das sei unser Ziel. Widmen wir uns nun dem Code. Der Teilausdruck
Color["Mazda"]
lässt sich über einen Indexer realisieren. Ein Indexer setzt ein Objekt voraus, denn wie wir wissen, überladen wir den »[]«-Operator in this, dem aktuellen Objekt also. Daraus kann gefolgert werden, dass wir zusätzlich zur Klasse Car eine zweite Klasse definieren müssen, die ihrerseits die Eigenschaft beschreibt. Im Folgenden soll der Name dieser Klasse CarColor lauten.
Wir könnten nun beide Klassen mit
public class Car {/*...*/} public class CarColor {/*...*/}
festlegen, aber damit käme die eindeutige Zugehörigkeit von CarColor zu Car nicht zum Ausdruck, weil CarColor auch ohne ein zugrunde liegendes Car-Objekt instanziiert werden könnte. Wir wissen aber, dass CarColor in einer direkten Beziehung zu Car steht. Deshalb drängt sich geradezu die Idee auf, CarColor in Car zu verschachteln:
public class Car { public class CarColor {/*...*/} }
Ein Objekt vom Typ CarColor soll einem Benutzer als schreibgeschützte Eigenschaft eines Car-Objekts angeboten werden. Wir ergänzen deshalb die äußere Klassendefinition Car um ein Feld, das die Referenz auf ein Car-Objekt zurückliefert:
public class Car { public readonly CarColor Color = new CarColor(); public class CarColor {/*...*/} }
Abgesehen von der internen Implementierung der Klasse CarColor können wir Car als fertig betrachten. Der gesamte weitere Programmcode beruht auf der vereinfachenden Annahme, dass sich die von jedem Hersteller angebotene Farbpalette nicht ändert. In der Praxis würde man diese im Car-Konstruktor vermutlich aus einer Datenbank beziehen. Wir legen die Farben jedoch in der Klasse CarColor fest, denn um den Einsatz der Indexer im Zusammenhang mit parameterbehafteten Eigenschaften zu verstehen, ist das völlig ausreichend.
Aus Sicht eines Benutzers sind wir nun an dieser Stelle angelangt:
Car myCar = new Car(); myCar.Color
Die zweite, noch unvollständige Anweisung liefert die Referenz auf ein CarColor-Objekt zurück. Jetzt schlägt die Stunde der Indexer! Zur Vervollständigung der Aufrufsyntax mit dem []-Operator müssen wir im nächsten Schritt in CarColor einen Indexer codieren.
Auch hier vereinfachen wir die Situation und tun so, als würde es nur zwei Autohersteller geben. Beim Aufruf der Color-Eigenschaft wird als Zeichenfolge der Hersteller übergeben. Der Rückgabewert sei ein Integer-Array, in dem jede Zahl für eine bestimmte Farbe steht.
public class CarColor { public int[] this[string hersteller] { get { switch(hersteller) { case "Rover": return new int[]{2, 3, 4, 5}; case "Mazda": return new int[]{1, 2, 5, 6}; default: return new int[]{0}; } } } }
Der Indexer versetzt uns jetzt in die Lage, beim Aufruf der Eigenschaft Color einen Index anzugeben, der als Argument interpretiert wird und den Eigenschaftswert maßgeblich beeinflusst.
Fassen wir den gesamten Code zusammen, und schreiben wir dazu noch zugreifenden Code, der exakt die Anweisung enthält, die Ausgangspunkt unserer Überlegungen war.
// --------------------------------------------------------- // Beispiel: ...\Kapitel 7\Eigenschaftsparameter // --------------------------------------------------------- class Program { static string[] color = new string[]{"Fehleingabe", "weiss","blau","gelb","rot","schwarz","lila"}; static void Main(string[] args) { Car myCar = new Car(); int[] arrInt = myCar.Color["Mazda"]; for(int i = 0; i < arrInt.Length; i++) Console.WriteLine("Farbe {0} = {1}", i, color[arrInt[i]] ); Console.ReadLine(); } } public class Car { public readonly CarColor Color = new CarColor(); public class CarColor { // Indexer public int[] this[string hersteller] { get { switch(hersteller) { case "Rover": return new int[]{2, 3, 4, 5}; case "Mazda": return new int[]{1, 2, 5, 6}; default: return new int[]{0}; } } } } }
Jede der beim Aufruf der Eigenschaft Color zurückgelieferten Integer-Zahlen ist demselben numerischen Index im string-Array der Testklasse zugeordnet. Die einzige Ausnahme bildet der Index 0, der eine unzulässige Parameterübergabe an die Eigenschaft signalisiert.
Mit
int[] arrInt = myCar.Color["Mazda"];
weisen wir den Rückgabewert einem Array zu, das in der darauffolgenden for-Schleife durchlaufen wird und die herstellerspezifische Farbpalette im Befehlsfenster ausgibt.