CodeGym /Kursy /C# SELF /Interfejsy w standardowej bibliotece .NET

Interfejsy w standardowej bibliotece .NET

C# SELF
Poziom 24 , Lekcja 3
Dostępny

1. Klasyfikacja kluczowych interfejsów

Jeśli myślisz, że interfejsy to zabawka dla architektów i "czystego kodu", a w prawdziwych projektach można się bez nich obejść, muszę cię zaskoczyć: każdy poważny projekt w C# po uszy siedzi w interfejsach. Dlaczego? Bo praktycznie każda część standardowej biblioteki .NET jest zrobiona przez interfejsy! Bez nich nie ogarniesz kolekcji, nie przeczytasz pliku, nawet nie przefiltrujesz kolekcji z LINQ.

Interfejsy to fundament polimorfizmu i rozszerzalności w .NET i to one decydują, jak wszystkie "klocki" frameworka łączą się ze sobą.

Standardowa biblioteka .NET jest dosłownie napakowana interfejsami. Żeby się w tym nie utopić, proponuję taką klasyfikację (oczywiście niepełną — życia nie starczy, żeby ogarnąć wszystko!):

Kategoria Interfejsy Po co są?
Kolekcje
IEnumerable
,
IEnumerator
,
IList
,
ICollection
,
IDictionary
Iteracja, modyfikacja, dostęp przez indeks, praca z parami klucz-wartość
Praca z zasobami
IDisposable
Zwalnianie zasobów (pliki, połączenia, strumienie)
Porównywanie
IComparable
,
IComparer
,
IEquatable
Sortowanie, porównywanie, unikalność obiektów
Serializacja
ISerializable
Zamiana obiektów na strumień bajtów (i odwrotnie)
LINQ i zapytania
IQueryable
,
IQueryProvider
Wsparcie dla złożonych zapytań (np. do bazy danych)
Asynchroniczność
IAsyncEnumerable
,
IAsyncDisposable
Asynchroniczna iteracja i czyszczenie zasobów
Zdarzenia i powiadomienia
INotifyPropertyChanged
,
INotifyCollectionChanged
Reakcja na zmiany właściwości/kolekcji
Data i czas
IFormattable
Customowe formatowanie stringów
Struktury danych
IStructuralComparable
,
IStructuralEquatable
Głębokie porównywanie kolekcji i krotek
Strumienie/wejście-wyjście
IStream
,
IAsyncDisposable
,
IObserver
,
IObservable
Praca ze strumieniami, powiadomienia push/pull

Ważne! Wiele z powyższych interfejsów ma typy-parametry: List<string>. Zwykle używa się ich w kolekcjach i dla interfejsów kolekcji Int[] → List<int>. Szczegóły ogarniemy za parę poziomów, jak będziemy poznawać kolekcje.

2. Interfejsy kolekcji

IEnumerable i IEnumerator — twój bilet do foreach

Prawie każda kolekcja w .NET implementuje interfejs IEnumerable albo nawet jego generyczną wersję IEnumerable<T>. To właśnie ten interfejs pozwala ci używać magicznej składni foreach:

List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers) // działa, bo List<int> implementuje IEnumerable<int>
{
    Console.WriteLine(n);
}

Tak wygląda ten interfejs w minimalistycznej wersji:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

IEnumerator — to interfejs samego "iteratora", któremu powierzono chodzenie po twojej kolekcji:

public interface IEnumerator
{
    bool MoveNext();
    object Current { get; }
    void Reset();
}

W prawdziwych zadaniach bardzo często będziesz przekazywać parametry i zwracać wartości typu IEnumerable<T>. Na przykład metoda, która zwraca wszystkie parzyste liczby z tablicy:

public IEnumerable<int> GetEvenNumbers(int[] array)
{
    foreach (var x in array)
        if (x % 2 == 0) yield return x;
}

Tak, słowo kluczowe yield robi magię — implementuje interfejs za ciebie, ale o tym więcej później.

ICollection i IList — kolekcje z dostępem przez indeks i modyfikacją

Jeśli potrzebujesz nie tylko iteracji, ale też możliwości dodawania/usuwania elementów albo dostępu przez indeks, potrzebujesz bardziej wyspecjalizowanych interfejsów:

  • ICollection<T> — dodaje metody Add, Remove oraz właściwość Count.
  • IList<T> — rozszerza listę możliwych operacji, w tym dostęp przez indeks (this[int index]).
public void PrintFirstItem(IList<string> list)
{
    if (list.Count > 0)
        Console.WriteLine(list[0]);
}

List<T> implementuje i IEnumerable<T>, i ICollection<T>, i IList<T>. To mega wygodne — możesz pracować z nim jak z prostą listą do iteracji albo używać wszystkich bajerów.

IDictionary<TKey, TValue> — pary "klucz-wartość"

Jeśli lubisz pracować ze słownikami (a to się zdarza bardzo często!), używaj interfejsu IDictionary<TKey, TValue>. Gwarantuje on, że możesz po kluczu dostać wartość i odwrotnie.

public void PrintAllPairs(IDictionary<string, int> ages)
{
    foreach (var pair in ages)
        Console.WriteLine($"{pair.Key}: {pair.Value}");
}

Tutaj ages może być czymkolwiek, nawet Dictionary<string, int> albo jakimś SortedDictionary<string, int> — ważne, że implementują ten interfejs!

3. Interfejs IDisposable: poprawna praca z zasobami

Cały input-output, praca z plikami, połączeniami sieciowymi, bazami danych w .NET opiera się na interfejsie IDisposable. Ten interfejs definiuje mega ważny kontrakt: jeśli obiekt ma niezarządzane zasoby, trzeba go "posprzątać" po użyciu. Tak, to ten gość, który umożliwił składnię using:

using (StreamReader reader = new StreamReader("file.txt"))
{
    // Pracujemy z plikiem, a po using plik na pewno się zamknie!
    string line = reader.ReadLine();
}

Jak wygląda sam interfejs? Śmiesznie prosto:

public interface IDisposable
{
    void Dispose();
}

Ale każda "poważna" biblioteka go implementuje! Więcej o dobrych praktykach pracy z tym interfejsem znajdziesz w oficjalnej dokumentacji.

4. Interfejsy do porównywania i unikalności

IComparable<T> i IComparer<T>

Jeśli chcesz, żeby twoja kolekcja obiektów mogła być posortowana, obiekty muszą umieć się porównywać między sobą. Do tego jest interfejs IComparable<T>:

public class Student : IComparable<Student>
{
    public string Name { get; set; }
    public int Score { get; set; }

    public int CompareTo(Student? other)
    {
        // Sortowanie malejąco po punktach
        if (other == null) return 1;
        return other.Score.CompareTo(this.Score);
    }
}

// Teraz możesz sortować studentów:
var students = new List<Student> { ... };
students.Sort();

Więcej w dokumentacji Microsoft.

IComparer<T> pozwala zdefiniować porównanie poza klasą — np. czasem chcesz sortować studentów po imieniu, a czasem po punktach:

public class NameComparer : IComparer<Student>
{
    public int Compare(Student? x, Student? y)
    {
        return string.Compare(x?.Name, y?.Name);
    }
}

IEquatable<T> — porównanie równości

Chcesz, żeby twój obiekt działał w HashSet<T> (czyli był "unikalny" dla kolekcji)? Zaimplementuj IEquatable<T>:

public class Person : IEquatable<Person>
{
    public string Name { get; set; }
    public bool Equals(Person? other)
    {
        if (other == null) return false;
        return this.Name == other.Name;
    }
}

5. Interfejsy do zdarzeń i powiadomień

Chciałeś kiedyś, żeby twoja aplikacja "wiedziała", że w obiekcie zmieniły się właściwości? Na przykład chcesz, żeby interfejs sam się odświeżał, jak użytkownik zmienia nazwisko? Właśnie do tego jest interfejs INotifyPropertyChanged. Jest mega popularny w aplikacjach z GUI (np. WPF albo Xamarin).

Sygnatura interfejsu jest prosta:

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler? PropertyChanged;
}

Przykład implementacji:

public class User : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string name;
    public string Name
    {
        get => name;
        set
        {
            if (name != value)
            {
                name = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

Teraz każde powiązanie UI z tym obiektem będzie się automatycznie odświeżać przy zmianie Name. Jeśli cię to zainteresuje — czytaj oficjalną dokumentację.

6. Inne przydatne interfejsy

Interfejs IFormattable: elastyczne formatowanie

Kiedy chcesz, żeby twój obiekt mógł się ładnie i różnie wyświetlać jako string (np. z różną liczbą miejsc po przecinku), zaimplementuj interfejs IFormattable:

public class Temperature : IFormattable
{
    public double Celsius { get; }
    public Temperature(double celsius) => Celsius = celsius;

    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        // Nie komplikujmy, pokażemy 'C' albo 'F' zależnie od formatu
        if (format == "F")
            return $"{Celsius * 9 / 5 + 32} F";
        return $"{Celsius} C";
    }
}

Teraz możesz wołać temp.ToString("F", null) albo temp.ToString("C", null). Dlatego DateTime, double, decimal i inne wbudowane typy wspierają elastyczne formatowanie.

Interfejsy do asynchroniczności

Z pojawieniem się asynchroniczności w C# pojawiły się nowe interfejsy do pracy w świecie async. Na przykład, jeśli twój obiekt asynchronicznie zwalnia zasoby — zaimplementuj IAsyncDisposable:

public interface IAsyncDisposable
{
    ValueTask DisposeAsync();
}

I teraz zamiast using piszesz await using!

Z asynchronicznymi enumeracjami (await foreach) działa interfejs IAsyncEnumerable<T>, który pozwala iterować po elementach bez blokowania głównego wątku. Używa się go np. przy czytaniu dużych plików na raty albo przy pracy ze strumieniami danych z internetu. Więcej na poziomie 58 :P

Interfejsy serializacji: ISerializable

Jeśli twoim zadaniem jest zamiana obiektów na strumień bajtów do zapisu/przesyłania (np. przez sieć), .NET daje interfejs ISerializable. Dziś używa się go rzadziej, bo są wygodniejsze mechanizmy (np. przez atrybuty i gotowe serializery), ale warto wspomnieć. Oto przykład sygnatury:

public interface ISerializable
{
    void GetObjectData(SerializationInfo info, StreamingContext context);
}

7. Zastosowanie interfejsów w praktyce: prawdziwa aplikacja

Załóżmy, że klepiesz konsolową apkę do ewidencji książek w bibliotece (no bo ktoś musi!). Chcesz, żeby można było wyświetlać listy książek, sortować je, filtrować, ładować i zapisywać dane. Dzięki znajomości interfejsów .NET możesz:

  • Zwracać różne typy kolekcji przez IEnumerable<Book>, żeby użytkownik nie musiał się zastanawiać, czy to tablica czy lista.
  • Dodać sortowanie po różnych kryteriach: implementujesz IComparable<Book> do sortowania po tytule i osobny IComparer<Book> po autorze.
  • Zapewnić efektywne zwalnianie zasobów przy pracy z plikami przez IDisposable.
  • Zorganizować filtrowanie książek po gatunku lub autorze przez LINQ, który działa na wszystkim, co implementuje IEnumerable<T>.
  • Skalować aplikację — np. zamienić plikowe repozytorium na bazę danych, jeśli twoje klasy operują przez interfejsy (IBookStorage), a nie konkretne implementacje.

8. Typowe błędy i niuanse

Jeden z najczęstszych błędów początkujących — używanie konkretnych implementacji ("List") zamiast interfejsów ("IEnumerable", "IList") przy deklarowaniu zmiennych i argumentów metod. PAMIĘTAJ: jeśli "programujesz na interfejsach", twój kod będzie elastyczny i łatwy do rozbudowy.

Czasem trzeba uważać, jaki poziom interfejsu wybrać — np. jeśli piszesz metodę, której wystarczy tylko iteracja, użyj IEnumerable<T>, a nie IList<T>, żeby nie narzucać niepotrzebnych ograniczeń.

Jeszcze jeden niuans: jeśli klasa ma dużo interfejsów i mają one powtarzające się metody lub właściwości, trzeba użyć jawnej implementacji interfejsu (explicit implementation), bo inaczej kompilator się pogubi.

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