CodeGym /Kursy /C# SELF /Wprowadzenie do indeksatorów

Wprowadzenie do indeksatorów

C# SELF
Poziom 18 , Lekcja 0
Dostępny

1. Kilka słów o kolekcjach

Do tej pory pracowaliśmy z tablicami — to najprostszy sposób na przechowywanie kilku elementów tego samego typu. Ale w C# jest mnóstwo wygodniejszych i mocniejszych kolekcji, które rozwiązują różne zadania.

Kolekcja — to obiekt, który może przechowywać grupę innych obiektów. W przeciwieństwie do tablic, kolekcje często mogą dynamicznie zmieniać swój rozmiar, oferują wygodne metody do pracy z danymi i są zoptymalizowane pod różne scenariusze użycia.

Podstawowe typy kolekcji (krótki przegląd):

List<T> — dynamiczna tablica, która może rosnąć i się kurczyć:

List<string> names = new List<string>();
names.Add("Aleks");
names.Add("Maria");
Console.WriteLine(names[0]); // Aleks
Console.WriteLine(names.Count); // 2

Dictionary<TKey, TValue> — kolekcja par "klucz-wartość", gdzie każdemu kluczowi odpowiada jedna wartość:

Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Aleks"] = 25;
ages["Maria"] = 30;
Console.WriteLine(ages["Aleks"]); // 25

Zwróć uwagę: w powyższych przykładach używamy nawiasów kwadratowych [] do dostępu do elementów kolekcji — dokładnie tak samo jak z tablicami! To możliwe dzięki indeksatorom, o których dziś pogadamy.

To tylko pierwsze spotkanie z kolekcjami — szczegółowo ich możliwości, różnice i zastosowania poznamy w kolejnych wykładach. Teraz ważne jest, żebyś zrozumiał, że kolekcje pozwalają na dostęp do swoich elementów przez nawiasy kwadratowe i to nie jest żadna magia, tylko specjalny mechanizm języka.

2. Indeksatory

Zwykłe obiekty C# działają przez właściwości i metody. Ale co, jeśli twój obiekt to taka mini-kolekcja? Na przykład wyobraź sobie:

  • Piszesz klasę Week, która powinna zwracać nazwę dnia tygodnia po numerze: week[0] → "Poniedziałek".
  • Albo klasę Library, gdzie możesz dostać się do książki po jej numerze: library[3] → "W stronę Swana".

Brzmi wygodnie, nie? Byłoby dziwne pisać library.GetBookByIndex(3) za każdym razem — chce się mieć dostęp do swojego obiektu jak do tablicy!

No i tu właśnie pojawiają się indeksatory.

Indeksator — to specjalny członek klasy, który pozwala używać obiektów tej klasy z nawiasami kwadratowymi, jak tablice: obj[0], obj["klucz"] i tak dalej.

Taki indeksator wygląda z zewnątrz jak właściwość, ale zamiast nazwy — przyjmuje parametry w nawiasach kwadratowych. To trochę jak piszemy .Name, tylko zamiast tego — [i].

3. Prosta kolekcja z indeksatorem

Zróbmy mini-klasę, która będzie przechowywać ulubione kolory. Do przechowywania użyjemy tablicy stringów. Bez indeksatora trzeba by było robić metodę typu GetColor(int i). Ale z indeksatorem:


using System;

public class FavoriteColors
{
   // Prywatne pole do przechowywania kolorów
   private string[] colors = new string[5];

   // Indeksator:
   public string this[int index]
   {
       get
       {
           // Kontrola zakresu tablicy (enkapsulacja w akcji!)
           if (index < 0 || index >= colors.Length)
               throw new IndexOutOfRangeException("Nieprawidłowy indeks koloru!");

           return colors[index];
       }
       set
       {
           if (index < 0 || index >= colors.Length)
               throw new IndexOutOfRangeException("Nieprawidłowy indeks koloru!");

           colors[index] = value ?? throw new ArgumentNullException(nameof(value));
       }
   }
}

class Program
{
   static void Main()
   {
       FavoriteColors favorites = new FavoriteColors();

       favorites[0] = "Zielony";
       favorites[1] = "Niebieski";
       favorites[2] = "Czerwony";
       favorites[10] = "Fioletowy"; // Rzuca wyjątek!

       Console.WriteLine(favorites[1]); // Niebieski
   }
}

Co tu się dzieje?

  • Tworzymy prywatną tablicę, żeby nikt z zewnątrz nie mógł się nią bawić bezpośrednio.
  • Indeksator jest zdefiniowany jako public string this[int index], gdzie this — słowo kluczowe wskazujące, że indeksator należy do obiektu.
  • Wewnątrz get i set kontrolujemy zakres, nie pozwalamy wyjść poza tablicę/zapisać null.
  • Na końcu możemy robić favorites[0], jak ze zwykłą tablicą.

4. Składnia indeksatora: szczegóły

Składnia jest podobna do właściwości, ale zamiast nazwy (np. Age) podajemy słowo kluczowe this z parametrami:


// Sygnatura indeksatora (ogólny szablon)
[modyfikator] TypWyniku this[TypIndeksu nazwaIndeksu]
{
   get { ... }
   set { ... }
}

Przykład: Klasyczny

public class MyCollection
{
   private int[] data = new int[10];

   // Indeksator do odczytu i zapisu
   public int this[int index]
   {
       get { return data[index]; }
       set { data[index] = value; }
   }
}

Indeksatory nie tylko po int

Główna zaleta: indeksator nie musi przyjmować tylko int. Możesz użyć dowolnego typu (byle dostęp po kluczu miał sens):

public string this[string colorName]
{
   get { /* ... */ }
   set { /* ... */ }
}

Na przykład w klasie książki telefonicznej logiczne jest szukać po imieniu:

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;
       }
   }
}

O kolekcjach i jak działa Dictionary<string, string> opowiem w przyszłych wykładach :P

5. Praktyczny przykład: Licznik słów w tekście

Rozwijamy naszą aplikację. Załóżmy, że mamy klasę, która liczy, ile razy każde słowo pojawiło się w tekście. Fajnie, jeśli użytkownik może dostać się do obiektu przez nawiasy kwadratowe, żeby dostać liczbę wystąpień po słowie:


using System.Collections.Generic;

public class WordCounter
{
   private Dictionary<string, int> counter = new Dictionary<string, int>();

   // Indeksator po stringu (słowo)
   public int this[string word]
   {
       get
       {
           if (counter.ContainsKey(word))
               return counter[word];
           return 0; // Jeśli nie ma takiego słowa, zwracamy 0.
       }
       set
       {
           counter[word] = value;
       }
   }

   // Metoda do liczenia słów ze stringa
   public void AddWords(string text)
   {
       foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
       {
           if (counter.ContainsKey(word))
               counter[word]++;
           else
               counter[word] = 1;
       }
   }
}

// W Main:
var wc = new WordCounter();
wc.AddWords("mama myła ramę myła mama tata");
Console.WriteLine($"'mama' występuje {wc["mama"]} raz(y)");
Console.WriteLine($"'rama' występuje {wc["rama"]} raz(y)");
Console.WriteLine($"'kot' występuje {wc["kot"]} raz(y)"); // 0

Po co to w praktyce? Takie podejście często stosuje się do własnych kolekcji, bibliotek pamięci, mapowań (słowniki i indeksy), a nawet DSL (specjalizowane języki wewnątrz C#).

6. Ograniczenia i niuanse

Indeksatory są mocne, ale mają kilka zasad i pułapek (no bo jakby inaczej).

Indeksator nie ma nazwy

W przeciwieństwie do właściwości, indeksator nie ma nazwy, tylko sygnaturę typu this[typ parametru]. Jeśli zaczniesz pisać public int MyIndexer[int i] — kompilator się zdziwi. Używamy tylko this.

Nie ma statycznych indeksatorów

Indeksatory zawsze są dla instancji klasy, a nie dla statycznych członków. Czyli nie można zadeklarować static int this[int i] — logika jest taka, że this zawsze wskazuje na konkretny obiekt.

Mogą być przeciążone po typie/liczbie parametrów

Możesz zrobić kilka indeksatorów w jednej klasie, jeśli ich parametry różnią się typem lub ilością. Na przykład:

public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }

To legalne, kompilator się nie pogubi — i przypomni ci, jeśli parametry się pokrywają.

Obsługa tylko przez właściwości

Nie można zadeklarować indeksatora bez akcesorów get lub set. Jeśli chcesz tylko do odczytu — usuń set, tylko do zapisu — get. Zazwyczaj używa się obu.

7. Praktyczna korzyść i po co to wiedzieć

  • Indeksatory są szeroko stosowane w kolekcjach danych. Wiele klas .NET je ma: na przykład List<T>, Dictionary<TKey,TValue>. Gdy piszesz list[2], używasz właśnie indeksatora!
  • Indeksatory pozwalają ukryć wewnętrzną implementację (enkapsulacja!), ale dają wygodny i znajomy interfejs. Użytkownik twojej klasy nie zastanawia się, jak przechowujesz dane, po prostu używa znajomego [index].
  • Twój kod staje się zwięzły i intuicyjny — co szczególnie doceniają na rozmowach kwalifikacyjnych (i twoi przyszli koledzy).

Właściwości i Indeksatory: porównanie

Właściwość Indeksator
Nazwa Tak (np. Name) Nie (zamiast nazwy — this[parametr])
Dostęp Po nazwie Po indeksie (lub innym kluczu)
Statyczność Może być static Tylko dla instancji
Wiele w klasie Tak, dowolna ilość Tak, ale z różną sygnaturą parametrów
Zastosowanie Przechowywanie/dostęp do danych Mini-kolekcje, dane asocjacyjne

8. Typowe błędy i porady

Błąd nr 1: próba zrobienia indeksatora static.
Tak się nie da — this[...] działa tylko z obiektem.

Błąd nr 2: brak sprawdzenia indeksu.
Jeśli nie sprawdzisz zakresu tablicy w get lub set, program może się wywalić.

Błąd nr 3: pomieszane typy parametrów.
Jeśli zrobisz dwa indeksatory z tymi samymi parametrami, kompilator zgłosi błąd.

Błąd nr 4: zapomniano zaimplementować get lub set.
Jeśli potrzebny jest dostęp do odczytu i zapisu — oba muszą być zaimplementowane.

Porada: jeśli twoja klasa opakowuje tablicę — po prostu przekazuj dostęp do tablicy przez indeksator. To przyspieszy pracę i uczyni kod intuicyjnym.

9. Po co to wszystko wiedzieć

Indeksatory sprawiają, że interfejs obiektu jest prosty, zrozumiały i wygodny. Pozwalają ukryć wewnętrzną implementację, ale zostawiają programiście wygodny sposób dostępu do danych.

Właśnie tak są zrobione wbudowane kolekcje: string, List<T>, Dictionary<TKey, TValue>, Span<T> i wiele innych. Gdy piszesz array[2] albo text[0], już używasz indeksatora.

A co najważniejsze — teraz możesz pisać własne klasy, które działają tak samo elastycznie i zwięźle. A to znaczy — jesteś o krok bliżej do pisania profesjonalnego i czytelnego kodu.

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