CodeGym /Kursy /C# SELF /Kontrakt enumerowalności: IEnumerable<T>

Kontrakt enumerowalności: IEnumerable<T>

C# SELF
Poziom 28, Lekcja 1
Dostępny

1. Wprowadzenie

Pewnie już zauważyłeś, że różne kolekcje w C# mają wspólne cechy. Na przykład prawie każdą z nich możemy przeiterować za pomocą pętli foreach:


var names = new List<string> { "Ania", "Borys", "Wika" };

// Uwaga — ten kod działa zarówno z List, tablicą, jak i nawet z HashSet!
foreach (var name in names)
{
    Console.WriteLine(name);
}

Gdzie tu magia? Chodzi o to, że wszystkie nowoczesne kolekcje implementują interfejs IEnumerable<T> — taki "kontrakt", który gwarantuje, że kolekcja potrafi zwracać elementy po kolei, jeden za drugim.

Wyobraź sobie firmę, gdzie dla każdej kolekcji pracowników (czy to dział, zespół projektowy, lista uczestników imprezy firmowej) obowiązuje zasada: jeśli obiekt implementuje interfejs "IEnumerable", to zawsze możesz przejść po wszystkich pracownikach w określonej kolejności, nie ważne "jak są przechowywani", ważne — że da się ich przeiterować.

2. Interfejs IEnumerable<T> — co w środku?

Zobaczmy, jak wygląda ten interfejs w .NET:


public interface IEnumerable<out T>
{
    IEnumerator<T> GetEnumerator();
}

Ma tylko jedną metodę — GetEnumerator, która zwraca obiekt typu IEnumerator<T>. Ten obiekt odpowiada za sam proces "enumerowania": wie, który element jest aktualny, jak przejść do następnego, kiedy wszystko się skończyło.

W skrócie: jeśli klasa (albo kolekcja) implementuje IEnumerable<T>, to można po niej przejść pętlą foreach, pobierać elementy jeden po drugim — i nie ważne, jak są zaimplementowane w środku: czy jako lista, czy jako hash-tabela, czy nawet na dysku twardym.

Mini-schemat


Kolekcja (np. List<T>)
    |
    v
IEnumerable<T>
    |
    v
IEnumerator<T> (prawdziwy "przechadzacz" po elementach)

3. Praktyka i uniwersalność kodu

Ogromny plus takiego kontraktu — uniwersalność. Jeśli funkcja lub metoda przyjmuje jako argument IEnumerable<T>, to znaczy, że jest kompatybilna z dowolnym typem kolekcji — od tablic i list po hash-sety, kolejki, a nawet własne kolekcje użytkownika!

Na przykład, zobaczmy funkcję liczącą sumę elementów:


// Można liczyć sumę dowolnego zbioru int, który wspiera IEnumerable<int>
int Sum(IEnumerable<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
        result += n;
    return result;
}

// Działa z List<int>
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Sum(list));

// Działa z tablicą!
int[] array = { 4, 5, 6 };
Console.WriteLine(Sum(array));

// Działa nawet z wynikiem metody filtrującej elementy
Console.WriteLine(Sum(list.Where(x => x % 2 == 0))); // Używamy LINQ

Przykład z życia: uniwersalne metody

Wyobraź sobie, że musisz napisać narzędzie do wyszukiwania najdłuższego słowa w dowolnej kolekcji stringów. Dzięki IEnumerable<string>, możesz to zrobić dla dowolnego źródła — czy to tablica, lista, wynik metod filtrujących itd.:


string FindLongest(IEnumerable<string> words)
{
    string longest = "";
    foreach (var word in words)
        if (word.Length > longest.Length)
            longest = word;
    return longest;
}

Możesz używać z dowolnym pasującym zbiorem.

4. Dlaczego pętla foreach działa z IEnumerable<T>?

Częste pytanie: dlaczego w ogóle pętla foreach "rozumie" dowolne kolekcje? Odpowiedź jest prosta: kompilator C# szuka w klasie metody GetEnumerator i oczekuje, że dostanie jakiś obiekt z metodami MoveNext() i właściwością Current. To właśnie standardowy interfejs — IEnumerator<T>.

Dzięki temu podejściu możesz nawet napisać własną klasę, która będzie "wydawać" elementy po kolei (np. generator ciągu Fibonacciego), i jeśli zaimplementuje IEnumerable<int>, możesz jej używać w foreach tak samo jak zwykłej listy.

5. Co to jest Enumerator i jak działa?

W środku każdej kolekcji, która implementuje IEnumerable<T>, ukryty jest specjalny “przechadzacz” — Enumerator (czyli naukowo: obiekt implementujący interfejs IEnumerator<T>). To właśnie on "przenosi" elementy kolekcji po jednym, gdy piszesz pętlę foreach.

Interfejs IEnumerator<T>

Co potrafi standardowy Enumerator:


public interface IEnumerator<T> : IDisposable
{
    T Current { get; }         // Aktualny element
    bool MoveNext();           // Przejdź do następnego elementu
    void Reset();              // Wróć na początek (rzadko używane)
}

Jak to działa w środku?

  • MoveNext() — przesuwa "wskaźnik" do następnego elementu i zwraca true, jeśli element istnieje. Jeśli nie ma już elementów — zwraca false.
  • Current — zwraca aktualny element (ten, na który teraz wskazuje enumerator).
  • Reset() — resetuje enumerator na początek (ale praktycznie nigdy się nie używa).
  • Dispose() — zwalnia zasoby (potrzebne dla kolekcji, które mogą pracować z plikami lub siecią).

Przykład: jak działa foreach "pod maską"

Kiedy piszesz:


var numbers = new List<int> { 1, 2, 3 };
foreach (var n in numbers)
    Console.WriteLine(n);

Tak naprawdę kompilator zamienia to mniej więcej na taki kod:


var numbers = new List<int> { 1, 2, 3 };

// Pobieramy "przechadzacza"
var enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
    var n = enumerator.Current;
    Console.WriteLine(n);
}
// Kompilator automatycznie wywołuje Dispose() w bloku using (jeśli enumerator implementuje IDisposable)

Ważne: jeśli kolekcja pracuje z zewnętrznymi zasobami (np. pliki, bazy danych), Enumerator może je zwalniać automatycznie, gdy kończy się iteracja.

Wizualny schemat


Początek iteracji -> GetEnumerator() -> Enumerator
         |
         v
  MoveNext() -> Current
         |
         v
  MoveNext() -> Current
         |
        ...
         |
         v
  MoveNext() == false -> koniec iteracji

6. IEnumerable i tablice, listy, zbiory: kto jest kim

Zobaczmy, które standardowe kontenery .NET implementują ten interfejs:

Typ kolekcji Implementuje IEnumerable<T>? Można przejść pętlą foreach?
Tablica (
int[]
)
✅ ✅
List<T>
✅ ✅
Dictionary<TKey, V>
✅ (po parach, kluczach, wartościach) ✅
HashSet<T>
✅ ✅
Queue<T>
✅ ✅
Stack<T>
✅ ✅

Nawet string (string) implementuje zwykły IEnumerable, więc po każdym znaku w stringu też możesz przejść.

7. Implementacja własnego Enumerable

Ciekawa sprawa: spróbujmy zaimplementować minimalną kolekcję, która przechowuje liczby parzyste w zakresie od 0 do N i implementuje IEnumerable<int>. Wtedy możemy ją przeiterować pętlą i użyć z LINQ.


// Klasa-kolekcja, którą można "przechodzić"
class EvenNumbers : IEnumerable<int>
{
    private int max;

    public EvenNumbers(int max)
    {
        this.max = max;
    }

    public IEnumerator<int> GetEnumerator()
    {
        for (int i = 0; i <= max; i += 2)
            yield return i; // Specjalna magia do implementacji enumeratora
    }

    // Jawna implementacja niegenerycznego interfejsu IEnumerable dla kompatybilności wstecznej
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// Użycie:
var evens = new EvenNumbers(10);
foreach(var e in evens)
    Console.Write($"{e} "); // 0 2 4 6 8 10

Ważny szczegół: jeśli twoja klasa implementuje IEnumerable<T> — automatycznie robisz ją kompatybilną z większością narzędzi .NET: LINQ, foreach, metodami przyjmującymi enumerable.

8. Typowe błędy i niuanse

Czasem początkujący programiści myślą, że IEnumerable<T> — to jakaś osobna kolekcja, która ma elementy. W rzeczywistości — to tylko "obietnica", że jeśli zaczniemy iterować, będą nam "wydawane" elementy po jednym.

Jeśli potrzebujesz losowego dostępu po indeksie (myList[5]), używać metod typu Add albo Remove — interfejs IEnumerable<T> tu nie pomoże. To tylko do sekwencyjnego przechodzenia!

Błąd: próba modyfikowania kolekcji podczas iteracji. Na przykład:


foreach (var item in myList)
{
    if (item < 0)
        myList.Remove(item); // NIEBEZPIECZNE! InvalidOperationException
}

Lepiej najpierw zrobić osobną listę do usunięcia, albo użyć metod, które tworzą nową kolekcję:


// Bezpieczny sposób — tworzymy nową kolekcję ręcznie
var newList = new List<int>();
foreach (var item in myList)
{
    if (item >= 0)
        newList.Add(item);
}
myList = newList;

// Albo usuwamy po indeksach w odwrotnej kolejności
for (int i = myList.Count - 1; i >= 0; i--)
{
    if (myList[i] < 0)
        myList.RemoveAt(i);
}
2
Zadanie
C# SELF, poziom 28, lekcja 1
Niedostępne
Implementacja własnej kolekcji wspierającej IEnumerable
Implementacja własnej kolekcji wspierającej IEnumerable
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION