1. Indeksatory z wieloma parametrami
Prosty indeksator (np. this[int index]) jest spoko, gdy chcesz mieć dostęp do elementów po indeksie liczbowym — prawie jak w tablicy. Ale C# pozwala na dużo więcej: możesz używać kilku parametrów, parametrów różnych typów, ustawiać różne modyfikatory dostępu dla get i set, a nawet mieć kilka indeksatorów w jednej klasie (jeśli ich sygnatury się różnią).
W nowych wersjach C# pojawiły się też dodatkowe bajery, które ułatwiają życie z indeksatorami, w tym syntactic sugar dla zwięzłego kodu.
Indeksator wcale nie musi przyjmować tylko jednego indeksu i tylko typu int. Parametry ustalasz Ty — i możesz mieć kilka parametrów różnych typów, jeśli tego wymaga Twoje zadanie.
public class ChessBoard
{
private string[,] board = new string[8, 8];
// Indeksator z dwoma parametrami!
public string this[int row, int col]
{
get { return board[row, col]; }
set { board[row, col] = value; }
}
}
Teraz możemy pisać kod tak:
ChessBoard chess = new ChessBoard();
chess[0, 0] = "Wieża";
chess[7, 7] = "Król";
string piece = chess[0, 0]; // "Wieża"
Takie podejście świetnie sprawdza się dla macierzy, dwuwymiarowych map, gier planszowych, złożonych kolekcji.
2. Indeksatory z parametrami różnych typów
Twój indeksator może przyjmować parametry nie tylko typu int, ale dowolnego innego sensownego typu. Najważniejsze — żeby miało to sens w Twoim przypadku.
using System.Collections.Generic;
public class Employee
{
public string Name { get; set; }
public int Age { get; set; }
public string Position { get; set; }
}
public class EmployeeCollection
{
private List<Employee> employees = new List<Employee>();
// Indeksator po imieniu pracownika (string)
public Employee this[string name]
{
get
{
foreach (var employee in employees)
{
if (employee.Name == name)
return employee;
}
return null; // Albo możesz rzucić wyjątek
}
set
{
for (int i = 0; i < employees.Count; i++)
{
if (employees[i].Name == name)
{
employees[i] = value;
return;
}
}
// Jeśli nie ma pracownika o takim imieniu — dodajemy nowego
employees.Add(value);
}
}
// Dla kompatybilności — indeksator po indeksie liczbowym
public Employee this[int index]
{
get { return employees[index]; }
set { employees[index] = value; }
}
}
var company = new EmployeeCollection();
company[0] = new Employee { Name = "Ivan", Age = 30, Position = "Programista" };
company[1] = new Employee { Name = "Maria", Age = 25, Position = "Projektant" };
Employee employee = company["Ivan"];
company["Piotr"] = new Employee { Name = "Piotr", Age = 28, Position = "Tester" };
Ważne: jeśli masz kilka indeksatorów — ich sygnatury muszą się różnić zestawem i typami parametrów.
3. Różne modyfikatory dostępu dla get i set
Czasem trzeba pozwolić tylko na odczyt po indeksie, a zapis zablokować (albo odwrotnie). W C# możesz ustawić różne modyfikatory dostępu dla get- i set-akcesorów indeksatora.
public class SecureEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
internal set { employees[index] = value; }
}
}
To często się stosuje, żeby chronić kolekcje przed nieautoryzowanymi zmianami, czyniąc klasę bardziej kontrolowaną.
4. Read-only i Write-only indeksatory
Czasem chcesz pozwolić tylko na odczyt albo tylko na zapis przez indeksator.
public class ReadOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get { return employees[index]; }
// set nie istnieje — nie można zmieniać!
}
}
public class WriteOnlyEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
set
{
employees.Insert(index, value);
}
// get nie istnieje — nie można czytać!
}
}
W prawdziwych projektach "write-only" praktycznie się nie spotyka: zwykle potrzebne są "read-only", np. gdy z klasy na zewnątrz można tylko podejrzeć, ale nie zmienić danych przez indeksator.
5. Indeksatory z kontrolą zakresu i logiką
W dobrych przykładach ważne jest nie tylko dać dostęp po indeksie, ale też poprawnie obsłużyć wyjście poza zakres, błędy wyszukiwania i inne wyjątkowe sytuacje.
public class SafeEmployeeCollection
{
private List<Employee> employees = new List<Employee>();
public Employee this[int index]
{
get
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Pracownik o takim indeksie nie istnieje!");
return employees[index];
}
set
{
if (index < 0 || index >= employees.Count)
throw new IndexOutOfRangeException("Nie można zamienić nieistniejącego pracownika!");
employees[index] = value;
}
}
}
Możesz zwracać null albo używać nowoczesnych wzorców obsługi braku wartości — wybór zależy od logiki Twojej aplikacji.
6. Indeksatory z nietypowymi parametrami
Możesz spotkać kolekcje, gdzie indeksator przyjmuje nietypowe typy: wyliczenia (enum), własne struktury, a nawet kilka parametrów różnych typów.
public enum Department { IT, HR, Finance, Marketing }
public class DepartmentEmployeeCollection
{
private Dictionary<Department , Employee> departmentLeads = new Dictionary<Department , Employee>();
public Employee this[Department department]
{
get { return departmentLeads.TryGetValue(department, out var employee) ? employee : null; }
set { departmentLeads[department] = value; }
}
}
var company = new DepartmentEmployeeCollection();
company[Department.IT] = new Employee { Name = "Anna", Age = 35, Position = "Szef IT" };
Employee itLead = company[Department.IT];
Takie podejście jest spoko, gdy masz unikalne identyfikatory albo jasne mapowanie typu i wartości — wygodne, czytelne i typ-bezpieczne.
7. Nowoczesne możliwości: Range i Index
Dzięki pojawieniu się typów Range i Index w C# 8 indeksatory dostały nowe możliwości pracy z zakresami i indeksami od końca:
public class SmartArray
{
private int[] numbers = Enumerable.Range(0, 100).ToArray();
public int[] this[Range range] => numbers[range];
public int this[Index index] => numbers[index];
}
// Użycie:
var smart = new SmartArray();
int[] middle = smart[20..30]; // od 20 do 29 elementu
int last = smart[^1]; // ostatni element
Jeśli tworzysz własną kolekcję, wsparcie dla Range i Index sprawia, że jest ona maksymalnie "natívna" i wygodna dla programistów.
8. Właściwości kontra indeksatory
Żeby lepiej ogarnąć, kiedy używać właściwości, a kiedy indeksatorów, warto porównać ich cechy.
- Właściwości są wygodne do dostępu do indywidualnych cech obiektu po nazwie: person.Name, car.Speed.
- Indeksatory są spoko do dostępu do elementów kolekcji lub struktury po kluczu: employees[0], phoneBook["Ivan"].
- Właściwość zawsze ma konkretną nazwę i w klasie może być tylko jedna właściwość o tej nazwie.
- Indeksator używa słowa kluczowego this i może mieć kilka wariantów, jeśli ich sygnatury się różnią.
- Właściwości mogą być statyczne, indeksatory — nie, bo this zawsze wskazuje na konkretną instancję obiektu.
9. Typowe błędy przy pracy z indeksatorami
Błąd nr 1: brak kontroli zakresu.
Jeśli nie sprawdzisz indeksu pod kątem wyjścia poza tablicę, możesz dostać IndexOutOfRangeException w najmniej oczekiwanym momencie.
Błąd nr 2: brak obsługi możliwego null z indeksatora.
Jeśli indeksator po stringu zwraca null przy braku elementu, a kod wywołujący bezmyślnie korzysta z wyniku, pojawi się NullReferenceException.
Błąd nr 3: duplikacja sygnatur indeksatorów.
C# nie pozwala tworzyć dwóch indeksatorów z tym samym zestawem parametrów.
Błąd nr 4: nieoczywista logika w set-akcesorze.
Jeśli w set używasz logiki „dodawania przy braku”, a nie zamiany, może to być mylące. Takie rozwiązania warto robić jawnie i dobrze dokumentować.
10. Praktyczne zastosowanie i podsumowanie
W realnych projektach indeksatory często są używane do tworzenia wyspecjalizowanych kolekcji, cache'y, słowników z dodatkową logiką, macierzy i wielowymiarowych struktur danych. Sprawiają, że kod jest czytelniejszy i bardziej intuicyjny — zamiast wywoływać metody typu GetElementByIndex(5) możesz po prostu napisać collection[5].
Pamiętaj: indeksatory powinny logicznie pasować do natury Twojej klasy. Jeśli klasa nie jest kolekcją ani strukturą danych, prawdopodobnie indeksator nie jest jej potrzebny. Ale jeśli Twoja klasa przechowuje i zarządza zbiorem elementów, indeksator może sprawić, że jej użycie będzie dużo wygodniejsze i bardziej naturalne.
GO TO FULL VERSION