CodeGym /Kursy /C# SELF /Rola interfejsów w kolekcjach C#

Rola interfejsów w kolekcjach C#

C# SELF
Poziom 28, Lekcja 0
Dostępny

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
IEnumerable<T>
Enumeracja elementów (foreach) Wszystko:
List<T>
,
HashSet<T>
, tablice
ICollection<T>
Modyfikowalny zbiór (Add, Remove, Count)
List<T>
,
HashSet<T>
,
Dictionary<K,V>
IList<T>
Dostęp przez indeks, modyfikacja
List<T>
, tablice
IDictionary<K,V>
Praca z parami "klucz-wartość"
Dictionary<K,V>
,
SortedDictionary<K,V>
ISet<T>
Zbiór (unikalne elementy)
HashSet<T>
,
SortedSet<T>

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>
Hierarchia interfejsów dla kolekcji-list i zbiorów

Dla słowników:


           IEnumerable<KeyValuePair<K,V>>
                      ^
                      |
               ICollection<KeyValuePair<K,V>>
                      ^
                      |
               IDictionary<K,V>
                      |
                Dictionary<K,V>
Hierarchia interfejsów dla słowników

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.

2
Zadanie
C# SELF, poziom 28, lekcja 0
Niedostępne
Implementacja interfejsu IEnumerable dla własnej kolekcji
Implementacja interfejsu IEnumerable dla własnej kolekcji
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION