1. Ein paar Worte zu Collections
Bisher haben wir mit Arrays gearbeitet – das ist der einfachste Weg, mehrere Elemente vom gleichen Typ zu speichern. Aber in C# gibt es jede Menge praktischere und mächtigere Collections, die verschiedene Aufgaben lösen.
Collection – das ist ein Objekt, das eine Gruppe anderer Objekte speichern kann. Im Gegensatz zu Arrays können Collections oft ihre Größe dynamisch ändern, bieten bequeme Methoden zur Datenbearbeitung und sind für verschiedene Einsatzszenarien optimiert.
Die wichtigsten Collection-Typen (kurzer Überblick):
List<T> – ein dynamisches Array, das wachsen und schrumpfen kann:
List<string> names = new List<string>();
names.Add("Alex");
names.Add("Maria");
Console.WriteLine(names[0]); // Alex
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> – eine Collection von "Schlüssel-Wert"-Paaren, wobei jedem Schlüssel ein Wert zugeordnet ist:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alex"] = 25;
ages["Maria"] = 30;
Console.WriteLine(ages["Alex"]); // 25
Achte darauf: In den Beispielen oben benutzen wir eckige Klammern [] für den Zugriff auf Collection-Elemente – genau wie bei Arrays! Das geht dank Indexern, über die wir heute sprechen.
Das ist nur die erste Begegnung mit Collections – die Details, Unterschiede und Einsatzmöglichkeiten schauen wir uns in den nächsten Vorlesungen an. Wichtig ist jetzt: Collections erlauben Zugriff auf ihre Elemente mit eckigen Klammern, und das ist keine Magie, sondern ein spezieller Sprachmechanismus.
2. Indexer
Normale C#-Objekte funktionieren über Properties und Methoden. Aber was, wenn dein Objekt eine Art Mini-Collection ist? Stell dir vor:
- Du schreibst eine Klasse Week, die den Namen des Wochentags anhand der Nummer zurückgeben soll: week[0] → "Montag".
- Oder eine Klasse Library, wo man per Nummer auf ein Buch zugreifen kann: library[3] → "Auf der Suche nach der verlorenen Zeit".
Klingt praktisch, oder? Es wäre komisch, jedes Mal library.GetBookByIndex(3) zu schreiben – man will sein Objekt wie ein Array benutzen!
Genau hier kommen Indexer ins Spiel.
Indexer – das ist ein spezielles Klassenmitglied, das es erlaubt, Objekte dieser Klasse mit eckigen Klammern zu verwenden, wie Arrays: obj[0], obj["schlüssel"] usw.So ein Indexer sieht von außen wie ein Property aus, aber statt eines Namens nimmt er Parameter in eckigen Klammern. Das ist ähnlich wie .Name, nur eben [i].
3. Eine einfache Collection mit Indexer
Lass uns eine Mini-Klasse bauen, die Lieblingsfarben speichert. Wir nehmen dafür ein Array von Strings. Ohne Indexer müsste man eine Methode wie GetColor(int i) machen. Aber mit Indexer:
using System;
public class FavoriteColors
{
// Privates Feld zum Speichern der Farben
private string[] colors = new string[5];
// Indexer:
public string this[int index]
{
get
{
// Array-Grenzen prüfen (Kapselung in Aktion!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Ungültiger Farbindex!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Ungültiger Farbindex!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Grün";
favorites[1] = "Blau";
favorites[2] = "Rot";
favorites[10] = "Violett"; // Löst eine Exception aus!
Console.WriteLine(favorites[1]); // Blau
}
}
Was passiert hier?
- Wir erstellen ein privates Array, damit niemand von außen direkt dran rumfummeln kann.
- Der Indexer ist als public string this[int index] definiert, wobei this das Schlüsselwort ist, das zeigt, dass der Indexer zum Objekt gehört.
- Im get und set prüfen wir den Bereich und lassen keine ungültigen Werte oder null zu.
- Am Ende können wir favorites[0] machen, wie bei einem normalen Array.
4. Syntax des Indexers: Details
Die Syntax ist ähnlich wie bei einem Property, aber statt eines Namens (z.B. Age) steht das Schlüsselwort this mit Parametern:
// Signatur eines Indexers (allgemeine Vorlage)
[modifier] ErgebnisTyp this[IndexTyp indexName]
{
get { ... }
set { ... }
}
Beispiel: Klassisch
public class MyCollection
{
private int[] data = new int[10];
// Indexer für Lesen und Schreiben
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Indexer müssen nicht nur int sein
Das Coole: Ein Indexer muss nicht nur int nehmen. Du kannst jeden Typ nehmen (Hauptsache, der Zugriff per Schlüssel macht Sinn):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
Zum Beispiel ist es in einer Telefonbuch-Klasse logisch, nach Namen zu suchen:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
Über Collections und wie Dictionary<string, string> funktioniert, erzähle ich in späteren Vorlesungen :P
5. Praxisbeispiel: Wortzähler im Text
Wir bauen unsere App weiter aus. Angenommen, wir haben jetzt eine Klasse, die zählt, wie oft jedes Wort im Text vorkommt. Es wäre praktisch, wenn der User per eckige Klammern auf das Objekt zugreifen könnte, um die Anzahl für ein Wort zu bekommen:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Indexer für ein Wort (string)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // Wenn das Wort nicht existiert, gib 0 zurück.
}
set
{
counter[word] = value;
}
}
// Methode zum Hinzufügen von Wörtern aus einem String
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// In Main:
var wc = new WordCounter();
wc.AddWords("mama hat fenster geputzt hat mama papa");
Console.WriteLine($"'mama' kommt {wc["mama"]} mal vor");
Console.WriteLine($"'fenster' kommt {wc["fenster"]} mal vor");
Console.WriteLine($"'katze' kommt {wc["katze"]} mal vor"); // 0
Wofür braucht man das in der Praxis? So ein Ansatz wird oft für eigene Collections, Memory-Libraries, Mappings (Dictionaries und Indizes) und sogar DSLs (spezielle Sprachen innerhalb von C#) verwendet.
6. Einschränkungen und Besonderheiten
Indexer sind mächtig, aber es gibt ein paar Regeln und Fallstricke (wie immer beim Programmieren).
Ein Indexer hat keinen Namen
Im Gegensatz zu einem Property hat ein Indexer keinen Namen, nur die Signatur wie this[Parametertyp]. Wenn du public int MyIndexer[int i] schreibst, wundert sich der Compiler. Wir nehmen immer this.
Es gibt keine statischen Indexer
Indexer werden immer für eine Instanz der Klasse erstellt, nicht für statische Members. Du kannst also nicht static int this[int i] deklarieren – die Logik ist, dass this immer auf die konkrete Instanz zeigt.
Sie können überladen werden (Typ/Anzahl der Parameter)
Du kannst mehrere Indexer in einer Klasse haben, wenn sich die Parameter im Typ oder in der Anzahl unterscheiden. Zum Beispiel:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
Das ist erlaubt, der Compiler kommt klar – und sagt dir Bescheid, wenn die Parameter sich überschneiden.
Nur mit get/set
Du kannst keinen Indexer ohne get oder set deklarieren. Wenn du nur lesen willst – lass set weg, nur schreiben – get. Meistens nimmt man beides.
7. Praktischer Nutzen und warum das wissen?
- Indexer werden aktiv in Daten-Collections verwendet. Viele .NET-Klassen haben sie: z.B. List<T>, Dictionary<TKey,TValue>. Wenn du list[2] schreibst, benutzt du genau einen Indexer!
- Indexer verstecken die interne Implementierung (Kapselung!), bieten aber ein bequemes und gewohntes Interface. Der User deiner Klasse muss nicht wissen, wie du die Daten speicherst, er nutzt einfach die gewohnten [index].
- Dein Code wird kompakt und intuitiv – das kommt gut im Vorstellungsgespräch (und bei deinen zukünftigen Kollegen).
Properties vs. Indexer: Vergleich
| Property | Indexer | |
|---|---|---|
| Name | Ja (z.B. Name) | Nein (statt Name – this[Parameter]) |
| Zugriff | Per Name | Per Index (oder anderem Schlüssel) |
| Statisch | Kann static sein | Nur für Instanzen |
| Mehrere pro Klasse | Ja, beliebig viele | Ja, aber mit unterschiedlicher Parametersignatur |
| Einsatz | Speichern/Zugriff auf Daten | Mini-Collections, assoziative Daten |
8. Typische Fehler und Tipps
Fehler Nr. 1: Versuch, einen Indexer static zu machen.
Das geht nicht – this[...] funktioniert nur mit einem Objekt.
Fehler Nr. 2: Index-Prüfung vergessen.
Wenn du im get oder set die Array-Grenzen nicht prüfst, kann das Programm abstürzen.
Fehler Nr. 3: Parameter-Typen verwechselt.
Wenn du zwei Indexer mit denselben Parametern erstellst, gibt der Compiler einen Fehler aus.
Fehler Nr. 4: get oder set vergessen zu implementieren.
Wenn du Lese- und Schreibzugriff willst, musst du beides implementieren.
Tipp: Wenn deine Klasse ein Array kapselt – leite die Zugriffe einfach per Indexer weiter. Das macht den Code schneller und intuitiver.
9. Warum das alles wissen?
Indexer machen das Interface eines Objekts einfach, verständlich und bequem. Sie verstecken die interne Implementierung, lassen dem Entwickler aber einen bequemen Weg zum Datenzugriff.
Genau so sind die eingebauten Collections gemacht: string, List<T>, Dictionary<TKey, TValue>, Span<T> und viele mehr. Wenn du array[2] oder text[0] schreibst, benutzt du schon einen Indexer.
Und das Beste: Du kannst jetzt deine eigenen Klassen schreiben, die genauso flexibel und kompakt funktionieren. Das bringt dich einen Schritt näher zu professionellem und lesbarem Code.
GO TO FULL VERSION