1. Po co kolekcjom interfejsy?
Bądźmy szczerzy: dopóki pisaliśmy kod z konkretnym typem (List<int>, Dictionary<string, string>), wszystko było proste i jasne. Ale wyobraź sobie, że nagle postanowiłeś zamienić List<T> na HashSet<T>, bo potrzebujesz tylko unikalnych wartości. Albo chcesz stworzyć metodę, która mogłaby działać zarówno z jednym, jak i z drugim typem kolekcji. Co wtedy?
Trzeba zrobić jedno wejście — potrzebny jest „uniwersalny port”. W C# takim portem są interfejsy. Określają one zestaw zasad (metod, właściwości), które kolekcja musi mieć. Wszystko, co implementuje interfejs — obsługuje wskazane operacje.
To jak ładowarka do telefonu: jeśli masz standardowe złącze USB-C, naładujesz i telefon, i nowoczesny odkurzacz, a nawet elektryczną szczoteczkę do zębów (byle nie pomylić ich w ciemnym pokoju).
Przykład w praktyce
Załóżmy, że chcesz napisać metodę, która liczy sumę wszystkich liczb w dowolnej kolekcji liczb. Tak by to wyglądało bez interfejsów:
// Taka metoda działa tylko z tablicami:
static int SumArray(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
// A taka - tylko z List<int>:
static int SumList(List<int> list)
{
int sum = 0;
for (int i = 0; i < list.Count; i++)
sum += list[i];
return sum;
}
Niewygodne! Przecież kod jest bardzo podobny. Dzięki interfejsowi IEnumerable<int> można to zrobić uniwersalnie:
static int SumAll(IEnumerable<int> collection)
{
int sum = 0;
foreach (var number in collection)
sum += number;
return sum;
}
I teraz ta metoda przyjmuje dowolny typ kolekcji, który implementuje IEnumerable<int>: tablicę, listę, zbiór i tak dalej.
2. Główne interfejsy kolekcji
W .NET dla kolekcji wyróżnia się kilka kluczowych interfejsów. Przyjrzyjmy się im krótko — w kolejnych wykładach poznasz szczegóły implementacji każdego z nich.
| Interfejs | Do czego służy | Przykład kolekcji implementujących interfejs |
|---|---|---|
|
Enumeracja elementów (foreach) | Wszystko: , , tablice |
|
Modyfikowalny zbiór (Add, Remove, Count) | , , |
|
Dostęp przez indeks, modyfikacja | , tablice |
|
Praca z parami "klucz-wartość" | , |
|
Zbiór (unikalne elementy) | , |
P.S. Dla uproszczenia na razie pomijamy „kilka poziomów” dziedziczenia między tymi interfejsami. Kto po kim dziedziczy — o tym później.
Wizualizacja: drzewo interfejsów kolekcji
IEnumerable<T>
^
|
ICollection<T>
^ ^
| |
IList<T> ISet<T>
| |
List<T> HashSet<T>
Dla słowników:
IEnumerable<KeyValuePair<K,V>>
^
|
ICollection<KeyValuePair<K,V>>
^
|
IDictionary<K,V>
|
Dictionary<K,V>
3. Po co interfejsy w prawdziwym życiu?
Elastyczność
Gdy twoja metoda lub klasa operuje nie na konkretnym typie (List<int>), ale na interfejsie (IEnumerable<int>), możesz przekazać jej dowolną kolekcję, która implementuje ten interfejs. Dzięki temu program staje się elastyczny i łatwy do rozbudowy. (Zamiast „Tylko iPhone” — teraz pasuje każdy telefon z Bluetooth.)
static void PrintAll(IEnumerable<string> collection)
{
foreach (var item in collection)
Console.WriteLine(item);
}
Teraz możesz drukować elementy dowolnej listy, tablicy, zbioru, a nawet wyniku zapytania LINQ!
Unifikacja
Jeśli implementujesz własny typ kolekcji (np. bardzo specyficzną kolekcję do jakiegoś zadania), zaimplementuj odpowiednie interfejsy — i twoja kolekcja będzie działać z cudzym kodem, który o niej nie wie. Dlatego w .NET często piszą: „programuj na poziomie interfejsów, a nie konkretnych implementacji”.
Kompatybilność ze standardowymi narzędziami
Dzięki wsparciu standardowych interfejsów twoje kolekcje mogą działać z LINQ, sortowaniem, serializacją i innymi możliwościami .NET.
4. List<T> — to i lista, i kolekcja, i enumeracja
Kilka słów o enumeracji
IEnumerable<T> — to najważniejszy i najbardziej podstawowy interfejs kolekcji w .NET. Określa tylko jedną metodę — GetEnumerator(), która zwraca obiekt-enumerator do iteracji po kolekcji. To właśnie dzięki niemu możesz pisać:
foreach (var name in names)
{
Console.WriteLine(name);
}
Gdzie names — to i lista, i zbiór, i nawet tablica. foreach — to sprytny syntaktyczny cukier, który pod spodem używa metody GetEnumerator().
Interfejs IEnumerable (bez <T>) — jest starszy, niegeneryczny. Teraz prawie zawsze używaj wersji generycznej.
Składnia deklaracji klasy List
Zobaczmy deklarację klasy List<T>, jeśli trochę zajrzeć pod maskę:
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, ...
To znaczy, że obiekt List<T> można używać jako:
- Listę z dowolnym dostępem przez indeks (IList<T>)
- Kolekcję z możliwością dodawania/usuwania elementów (ICollection<T>)
- Po prostu zbiór elementów, po którym można przejść (IEnumerable<T>)
Przykład:
List<string> names = new List<string> { "Anna", "Borys", "Wasia" };
// Jako IEnumerable - można przejść w pętli
IEnumerable<string> asEnumerable = names;
foreach (var n in asEnumerable) Console.WriteLine(n);
// Jako ICollection - można sprawdzić Count:
ICollection<string> asCollection = names;
int count = asCollection.Count;
// Jako IList - można dostać się przez indeks:
IList<string> asList = names;
string firstItem = asList[0];
5. Przykład aplikacji
Załóżmy, że mamy prostą aplikację do zarządzania użytkownikami i ich rolami (zaczęliśmy ją robić na poprzednich wykładach):
// Klasa użytkownika
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}
Możemy przechowywać użytkowników w dowolnej kolekcji implementującej IEnumerable<User>:
List<User> listUsers = new List<User> { new User { Name = "Anna", Age = 20 } };
HashSet<User> setUsers = new HashSet<User> { new User { Name = "Borys", Age = 25 } };
// Metoda, która wypisuje imiona wszystkich użytkowników
static void PrintUserNames(IEnumerable<User> users)
{
foreach (var user in users)
Console.WriteLine(user.Name);
}
// Używamy z różnymi typami kolekcji:
PrintUserNames(listUsers);
PrintUserNames(setUsers);
Teraz aplikacja staje się dużo bardziej elastyczna — bez magii i customowych przeciążeń!
6. Typowe błędy i pułapki
Jeden z najczęstszych błędów — używanie zbyt konkretnych typów, gdy wystarczy interfejs:
// Źle (sztywno przywiązane do implementacji)
void DoSomethingWithList(List<int> numbers) { ... }
// Lepiej (metoda działa z dowolną kolekcją)
void DoSomethingWithNumbers(IEnumerable<int> numbers) { ... }
Jeszcze jedna pułapka — zapomnieć, że interfejs określa tylko te operacje, które są w nim jawnie opisane. Na przykład w IEnumerable<T> nie da się sprawdzić liczby elementów (Count), do tego musisz użyć ICollection<T> — albo policzyć je ręcznie.
GO TO FULL VERSION