CodeGym /Kursy /C# SELF /Zaawansowane indeksatory w C#: parametry, modyfikatory, n...

Zaawansowane indeksatory w C#: parametry, modyfikatory, nowoczesne możliwości

C# SELF
Poziom 18 , Lekcja 1
Dostępny

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; }
    }
}
Indeksator z dwoma parametrami dla struktury dwuwymiarowej

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.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION