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.
GO TO FULL VERSION