10.7 Indexer
In Kapitel 2 haben Sie gelernt, mit Arrays zu arbeiten. Sie wissen, wie Sie ein Array deklarieren und auf die einzelnen Elemente zugreifen können, z. B.:
int[] arr = new int[10];
arr[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 mit den Collections eine Vielzahl solcher Klassen zur Verfügung.
Stellen Sie sich vor, Sie würden die Klasse Team entwickeln. Eine Mannschaft setzt sich aus vielen Einzelspielern zusammen, die innerhalb der Klasse in einem Array vom Typ Player verwaltet werden. Wenn Sie die Klasse Team mit
Team Wacker = new Team();
instanziieren, wäre es doch zweckdienlich, sich von einem bestimmten Spieler mit der Anweisung
string name = Wacker[2].Name;
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 Player-Objekt zurückliefert. Darauf können wir mit dem Punktoperator den Zunamen des gewünschten Spielers ermitteln, vorausgesetzt, diese Eigenschaft ist in der Klasse Player 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 static deklariert werden.
<Modifikatoren> <Datentyp> this[<Parameterliste>]
Als Modifizierer sind neben den Zugriffsmodifikatoren auch new, virtual, sealed, override und abstract zulässig. Wenn wir uns in Erinnerung rufen, was wir im vorhergehenden Abschnitt über Operatorüberladung gelernt haben, kommt man auch nicht an der Aussage vorbei, Indexer als eine Überladung des []-Operators zu betrachten.
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 Mannschaft ansehen.
// Beispiel: ..\Kapitel 10\IndexerSample
class Program {
static void Main(string[] args) {
Team Wacker = new Team();
// Spieler der Mannschaft hinzufügen
Wacker[0] = new Player { Name = "Fischer", Age = 23 };
Wacker[1] = new Player { Name = "Müller", Age = 19 };
Wacker[2] = new Player { Name = "Mamic", Age = 33 };
Wacker[3] = new Player { Name = "Meier", Age = 31 };
// Spielerliste ausgeben
for (int index = 0; index < 25; index++) {
if (Wacker[index] != null)
Console.WriteLine("Name: {0,-10}Alter: {1}",
Wacker[index].Name, Wacker[index].Age);
}
Console.ReadLine();
}
}
// Mannschaft
public class Team {
private Player[] team = new Player[25];
// Indexer
public Player this[int index] {
get { return team[index]; }
set {
// prüfen, ob der Index schon besetzt 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 Player {
public string Name { get; set;}
public int Age { get; set; }
}
Listing 10.34 Beispielprogramm zum Indexer
Jede Instanz der Klasse Team 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 Player. 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 Player-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.
10.7.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 Team um einen weiteren Indexer in der Weise, dass wir auch über den Namen des Spielers auf das zugehörige Objekt zugreifen können, also zum Beispiel mit
Player spieler = Wacker["Fischer"];
Angemerkt sei dabei, dass das Beispiel nur 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 nicht das Thema an dieser Stelle sein.
// Beispiel: ..\Kapitel 10\IndexerUeberladungSample
class Program {
static void Main(string[] args) {
Team Wacker = new Team();
// Spieler der Mannschaft hinzufügen
Wacker[0] = new Player { Name = "Fischer", Age = 23 };
Wacker[1] = new Player { Name = "Müller", Age = 19 };
Wacker[2] = new Player { Name = "Mamic", Age = 33 };
Wacker[3] = new Player { Name = "Meier", Age = 31 };
// Spieler suchen
Console.Write("Spieler suchen: ... ");
string spieler = Console.ReadLine();
if (Wacker[spieler] != null){
Console.WriteLine("{0} gefunden, Alter = {1}",
Wacker[spieler].Name, Wacker[spieler].Age);
Console.WriteLine(Wacker[spieler].Age);
}
else
Console.WriteLine("Der Spieler gehört nicht zum Team.");
Console.ReadLine();
}
}
// Mannschaft
public class Team {
private Player[] team = new Player[25];
// Indexer
public Player this[int index] {
[...]
}
public Player this[string name] {
get {
for (int index = 0; index < 25; index++) {
if (team[index] != null && team[index].Name == name)
return team[index];
}
return null;
}
}
}
Listing 10.35 Beispiel mit Indexerüberladung
Die Überladung des Indexers mit einem string enthält nur den get-Accessor, da die Zuweisung eines neuen Player-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 Player-Objekt mit dem gesuchten Namen, wird das Objekt an den Aufrufer zurückgegeben. Diese Operation wird durch
if (team[index] != null && team[index].Name == name)
return team[index];
beschrieben. Sollte sich ein Spieler mit dem gesuchten Namen nicht in der Mannschaft befinden, ist der Rückgabewert null.
10.7.2 Parameterbehaftete Eigenschaften
Eigenschaften sind per Definition parameterlos. Mit anderen Worten: Sie können einen Eigenschaftswert nicht in Abhängigkeit von einer oder mehreren Nebenbedingungen setzen. Aber es geht doch! Tatsächlich ließe sich die folgende Wertzuweisung an eine Eigenschaft programmieren:
@object.MyProperty[2] = 10;
In der fiktiven Eigenschaft MyProperty 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. Daher führt der Weg zur Lösung auch in diesem Fall über Indexer. Wir sollten uns das Verfahren an einem konkreten Beispiel ansehen und stellen uns daher vor, wir würden eine Klasse Table codieren mit einer Eigenschaft Cell. Wenn table eine Instanz der Klasse Table ist, soll mit
table.Cell[2,1] = 97;
einer bestimmten Zelle der Tabelle ein Wert zugewiesen werden.
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 Table eine zweite Klasse definieren müssen, die ihrerseits die Eigenschaft beschreibt. Im Folgenden soll der Name dieser Klasse Content lauten.
Wir könnten nun beide Klassen mit
public class Table { [...] }
public class Content { [...] }
festlegen.
Ein Objekt vom Typ Content soll einem Benutzer als schreibgeschützte Eigenschaft eines Table-Objekts angeboten werden. Wir ergänzen deshalb die Klassendefinition Table um ein Feld, das die Referenz auf ein Content-Objekt zurückliefert, und veröffentlichen diese über den get-Zweig der Eigenschaft Cell:
class Table {
private Content _Cell = new Content();
public Content Cell {
get { return _Cell; }
}
}
Die Klasse Table können wir bereits als fertig betrachten. Widmen wir uns nun der Klasse Content und dem in dieser Klasse programmierten Indexer. Da wir die Absicht haben, als Randbedingung der Eigenschaft Cell den Index der Zeile und Spalte der von uns angesprochenen Zelle mitzuteilen, sieht ein erster Entwurf des Indexers wie folgt aus:
class Content {
public int this[int row, int column] {
get { return arr[row, column]; }
set { arr[row, column] = value; }
}
}
Die Tabelle soll durch ein zweidimensionales Array dargestellt werden. Daher müssen wir sicherstellen, dass bei Übergabe der Bedingung nicht die Arraygrenzen überschritten werden – sowohl im set- als auch im get-Zweig. Eine private Methode in Content bietet sich zu diesem Zweck an:
private void CheckIndex(int row, int column) {
if (row < arr.GetLength(0) && column < arr.GetLength(1))
return;
else
throw new IndexOutOfRangeException("Ungültiger Index");
}
Die Variable arr sei die Referenz auf das Array, das die Tabellendaten repräsentiert. CheckIndex wird im set- und get-Zweig der Eigenschaft Cell aufgerufen.
Damit sind wir nahezu fertig mit dem Beispielprogramm. Was noch fehlt ist die Datenquelle, also das Array, und entsprechender Code zum Testen. Das Array können wir zu Testzwecken in der Klasse Content hinterlegen, das soll an dieser Stelle genügen.
Den gesamten Code zur Implementierung der parametrisierten Eigenschaft fassen wir an dieser Stelle endgültig zusammen.
// Beispiel: ..\Kapitel 10\ParametrisierteProperty
class Program {
static void Main(string[] args) {
Table table = new Table();
PrintArray(table);
table.Cell[2,1] = 97;
Console.WriteLine();
PrintArray(table);
Console.ReadLine();
}
// Ausgabe des Arrays im Tabellenformat
static void PrintArray(Table tbl) {
for(int row = 0; row < 4; row++) {
for(int col = 0; col < 3; col++)
Console.Write("{0,-3}",tbl.Cell[row, col]);
Console.WriteLine();
}
}
}
// Klasse Table
class Table {
private Content _Cell = new Content();
public Content Cell
{
get { return _Cell; }
}
}
// Klasse Content
class Content {
private int[,] arr = { {1,2,3}, {4,5,6}, {7,8,9}, {10,11,12} };
// Indexer
public int this[int row, int column]
{
get {
CheckIndex(row, column);
return arr[row, column];
}
set {
CheckIndex(row, column);
arr[row, column] = value;
}
}
// Prüfen der Arraygrenzen
private void CheckIndex(int row, int column) {
if (row < arr.GetLength(0) && column < arr.GetLength(1))
return;
else
throw new IndexOutOfRangeException("Ungültiger Index");
}
}
Listing 10.36 Definition einer parameterbehafteten Eigenschaft
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.