CodeGym /Kursy /C# SELF /Delegaty: zaawansowane scenariusze

Delegaty: zaawansowane scenariusze

C# SELF
Poziom 54 , Lekcja 3
Dostępny

1. Callback i programowanie asynchroniczne

Callback (callback) — to mechanizm przekazania metody, która ma być wywołana po zakończeniu jakiejś operacji. Bardzo często używany w operacjach asynchronicznych, timerach, przetwarzaniu danych, interfejsach użytkownika.

Przykład 1: Operacja asynchroniczna z callbackiem

Załóżmy, że mamy aplikację, gdzie użytkownik wpisuje zapytanie, a wynik otrzymujemy z opóźnieniem (np. z internetu). Po otrzymaniu danych chcemy odświeżyć ekran.


// Delegat dla callbacku
public delegate void DataReceivedHandler(string result);

// Mechanizm pobierania danych asynchronicznie (symulacja)
public void DownloadDataAsync(DataReceivedHandler callback)
{
    // Załóżmy, że pobieranie trwa (symulacja przez timer)
    Task.Delay(1000).ContinueWith(_ =>
    {
        string data = "Wyniki wyszukiwania: <dane>";
        callback(data); // Wywołanie delegata-callbacku
    });
}

// Użycie:
DownloadDataAsync(result =>
{
    Console.WriteLine("Otrzymano: " + result);
});

Takie podejście pozwala pisać bardzo elastyczny kod, gdzie logika po otrzymaniu wyniku jest całkowicie oddzielona od mechaniki pobierania danych.

2. Delegaty jako parametry metod: strategia i comparatory

Częste zadanie: dać użytkownikowi możliwość przekazania „logiki” (funkcji) do twojej metody, żeby sam określił, jak porównywać, filtrować lub przekształcać elementy.

Przykład 2: Implementacja wzorca Strategy przez delegaty

Załóżmy, że mamy sortowanie, ale chcemy, żeby użytkownik mógł sortować różnie — po nazwie, po dacie, po rozmiarze itd.


public delegate bool CompareFunc(int a, int b);

public void BubbleSort(int[] arr, CompareFunc compare)
{
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr.Length - 1; j++)
        {
            if (compare(arr[j], arr[j + 1]))
            {
                // Zamieniamy miejscami
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// Sortowanie od największego do najmniejszego
CompareFunc descending = (a, b) => a < b;

// Użycie
int[] numbers = { 3, 1, 4, 2 };
BubbleSort(numbers, descending);
Console.WriteLine(string.Join(", ", numbers)); // Wypisze: 4, 3, 2, 1

Taki zabieg to uniwersalny sposób wstrzykiwania własnej „strategii” do cudzego kodu bez zmieniania jego źródeł.

3. Metody anonimowe, wyrażenia lambda i delegaty

Wraz z rozwojem C# stało się niewygodne dla każdego zadania deklarować oddzielną klasę lub metodę. Na szczęście pojawiły się metody anonimowe i lambdy, które pozwalają tworzyć delegaty „w locie”.

Przykład 3: Lambda jako delegat


Func<int, int, int> operation = (x, y) => x * y;
int result = operation(3, 5); // 15

Przykład 4: Wybór operacji po nazwie (switch + Delegates)


Func<int, int, int> op;
string userInput = "sum"; // "sub", "mul", "div"

switch (userInput)
{
    case "sum": op = (a, b) => a + b; break;
    case "sub": op = (a, b) => a - b; break;
    case "mul": op = (a, b) => a * b; break;
    case "div": op = (a, b) => a / b; break;
    default: throw new Exception("Nieznana operacja!");
}
Console.WriteLine(op(6, 2));

Walidacji danych wejściowych nie zapominamy — delegaty tu dają elastyczność i czytelność.

4. Delegaty i łańcuchy przetwarzania („chain of responsibility”)

Ponieważ delegaty wspierają wieloadresowość, pozwalają łatwo budować łańcuchy handlerów.

Przykład 5: Łańcuch filtrów

Wyobraźmy sobie, że mamy „filtry”, które mają przetworzyć stringa.


public delegate string StringFilter(string input);

string RemoveDigits(string input) => new string(input.Where(ch => !char.IsDigit(ch)).ToArray());
string ToUpper(string input) => input.ToUpper();

StringFilter filters = RemoveDigits;
filters += ToUpper;

// Delegat przetworzy string przez wszystkie filtry
string text = "Cześć123";
foreach (StringFilter filter in filters.GetInvocationList())
{
    text = filter(text);
}
Console.WriteLine(text); // Wypisze: "CZEŚĆ"

Ważne: jeśli wywołasz po prostu filters(text), to zwrócona zostanie wartość tylko ostatniego handlera, a nie całego łańcucha! Jeśli potrzebujesz „przepływu” wartości, użyj jawnej iteracji przez GetInvocationList(), jak powyżej.

5. Delegaty dla dynamicznego wiązania zachowania w locie

Kiedyś, by zmienić zachowanie, trzeba było wymyślać osobne klasy i interfejsy. Dzięki delegatom i lambdom można znaczną część „drobnego” polimorfizmu wyrazić funkcjami.

Przykład 6: Zachowanie robota z dynamiczną komendą


public class Robot
{
    public event Action<string>? OnCommandReceived;

    public void ReceiveCommand(string command)
    {
        OnCommandReceived?.Invoke(command);
    }
}

// Użycie:
var robot = new Robot();

robot.OnCommandReceived += cmd => Console.WriteLine($"Robot wykonuje: {cmd}");
robot.OnCommandReceived += cmd =>
{
    if (cmd == "Włączyć się")
        Console.WriteLine("Ładowanie systemu...");
};
// Próby
robot.ReceiveCommand("Włączyć się");
robot.ReceiveCommand("Przenieść się do przodu");

Takie podejście często stosuje się w testach, prototypach, w DI (dependency injection) kontenerach i do przekazywania logiki biznesowej przez parametry.

6. Delegaty jako subskrypcje stanu

Załóżmy, że mamy klasę, która przechowuje pewien stan, i przy jego zmianie chcemy powiadomić wszystkich subskrybentów. Dzięki delegatom (i zdarzeniom) — to proste.

Przykład 7: Klasa z subskrypcją zmiany


public class Notifier<T>
{
    private T _value = default!;
    public event Action<T>? ValueChanged;

    public T Value
    {
        get => _value;
        set
        {
            if (!Equals(_value, value))
            {
                _value = value;
                ValueChanged?.Invoke(_value);
            }
        }
    }
}

// Użycie:
var intValue = new Notifier<int>();
intValue.ValueChanged += v => Console.WriteLine($"Nowa wartość: {v}");
intValue.Value = 5; // Wyzwala zdarzenie
intValue.Value = 10;

Takie podejście to praktycznie „reaktywne programowanie w miniaturze”, podstawa dla MVVM, data binding i wielu nowoczesnych UI-frameworków.

7. Delegaty, closures i zakres leksykalny

Wyrażenia lambda i metody anonimowe mogą przechwytywać zmienne z otaczającego kontekstu (closure). To wygodne, ale czasem prowadzi do niespodziewanych błędów.

Przykład 8: Przechwycenie zmiennej i pułapka pętli


Action[] actions = new Action[3];

for (int i = 0; i < 3; i++)
{
    actions[i] = () => Console.WriteLine(i);
}

foreach (var a in actions)
    a(); // Wypisze trzy razy 3 (!)

Dlaczego? Bo closure odwołuje się do tej samej zmiennej i, która po pętli równa się 3. A jeśli chcemy zapamiętać wartości 0, 1, 2?


for (int i = 0; i < 3; i++)
{
    int loopValue = i; // "zamrażamy" bieżącą wartość
    actions[i] = () => Console.WriteLine(loopValue);
}

Teraz kod działa zgodnie z oczekiwaniem. Takie pułapki to jeden z najczęstszych błędów u początkujących używających lambd!

8. Łączenie delegatów

Multicast delegates zawierają listę metod, i możesz dodawać (+=) lub usuwać (-=) handlery.

Cechy: usuwanie po referencji i sygnaturze


void Handler1() => Console.WriteLine("1");
void Handler2() => Console.WriteLine("2");

Action a = Handler1;
a += Handler2;
a -= Handler1; // Zostawi tylko Handler2
a?.Invoke();   // Wypisze "2"

Przykład: dynamiczne zarządzanie handlerami


Action a = Handler1;
a += Handler1;
a -= Handler1; // Teraz zostaje JEDEN Handler1 na liście!

9. Delegaty, rozszerzalność i inwersja kontroli (IoC)

W dużych aplikacjach często wymagane jest, żeby komponenty potrafiły „wywołać” kod zewnętrzny, pozostając przy tym niezależne. Delegaty pomagają wprowadzać „rozszerzenia”, pluginy i callbacki bez tight coupling.

Przykład: Wstrzykiwanie zachowania przez konstruktor


public class Greeter
{
    private readonly Func<string> _getName;

    public Greeter(Func<string> getName)
    {
        _getName = getName;
    }

    public void Greet() => Console.WriteLine($"Cześć, {_getName()}!");
}

// Wstrzykiwanie różnego zachowania:
var greeter1 = new Greeter(() => "Ania");
var greeter2 = new Greeter(() => DateTime.Now.ToShortTimeString());

greeter1.Greet(); // "Cześć, Ania!"
greeter2.Greet(); // "Cześć, 14:35!"

W praktyce taki wzorzec używany jest przy pisaniu testowalnego i utrzymywalnego kodu.

10. Przydatne niuanse

Delegaty w standardowych interfejsach i LINQ

Spotkanie z delegatami jest nieuniknione, jeśli pracujesz z LINQ, kolekcjami, asynchronicznością.

  • Wiele metod, jak List<T>.Find, Array.Sort, Where, Select przyjmuje delegaty (Func<T, bool>, Comparison<T> itd.).
  • Metody LINQ pozwalają przekazywać logikę filtrowania, transformacji, agregacji — bez tworzenia osobnych klas.

Przykład: Comparator do sortowania obiektów


var people = new[] { "Iwan", "Maria", "Piotr" };
Array.Sort(people, (a, b) => a.Length.CompareTo(b.Length));
Console.WriteLine(string.Join(", ", people)); // Iwan, Piotr, Maria

Delegaty i currying (częściowe zastosowanie argumentów)

Za pomocą metod anonimowych/lambd możesz „zafiksować” część parametrów i dostać nową funkcję.

Przykład: Częściowe zastosowanie


Func<int, int, int> sum = (x, y) => x + y;

// Tworzymy funkcję, która zawsze dodaje 10
Func<int, int> add10 = y => sum(10, y);

Console.WriteLine(add10(5)); // 15

Cechy porównywania delegatów

W C# delegaty można porównywać ze sobą na równość (==), jeśli mają identyczną listę wywołań (invocation list).


void Handler1() { }
void Handler2() { }

Action a1 = Handler1;
Action a2 = Handler1;
Console.WriteLine(a1 == a2); // True

Action a3 = Handler1; a3 += Handler2;
Action a4 = Handler1; a4 += Handler2;
Console.WriteLine(a3 == a4); // True

Ale jeśli delegat zbudowany jest na metodzie anonimowej lub lambdzie — porównywane są konkretne instancje.

Serializacja delegatów

Delegaty można serializować, ale tylko jeśli metody, do których się odnoszą, są zdefiniowane w klasach serializowalnych i wszystkie typy są dostępne. Począwszy od .NET 8, BinaryFormatter jest domyślnie wyłączony i uznawany za przestarzały, a w przyszłych wersjach zostanie usunięty; serializacja delegatów w zadaniach produkcyjnych praktycznie nie jest używana.

Interakcja delegatów i zdarzeń: gdzie delegat, a gdzie zdarzenie?

  • Delegat — to typ/zmienna, którą można wywołać jawnie.
  • Zdarzenie (event) — sposób ograniczenia dostępu do delegata: z zewnątrz można tylko subskrybować/odsubskrybować (+=/-=), ale wywoływać — tylko wewnątrz klasy.
  • Zdarzenie jest zawsze typu delegatowego, ale nie każdy delegat to zdarzenie.

Jak stosować? Jeżeli chcesz, żeby logika mogła być zdefiniowana „poza” klasą, używaj delegatów. Jeśli chcesz kontrolować subskrypcję/odsubskrypcję i chronić zmienną — użyj zdarzenia.

11. Typowe błędy przy pracy z delegatami

Błąd nr 1: Mylące delegat i zdarzenie.
Używanie publicznego pola-delegata (public Action MyAction;) zamiast zdarzenia (public event Action MyAction;) łamie enkapsulację. Zewnętrzny kod może przypadkowo lub celowo nadpisać wszystkich subskrybentów (instance.MyAction = null;) lub wywołać ich bezpośrednio, co narusza logikę klasy.

Błąd nr 2: Nieprawidłowe traktowanie zwracanych wartości w multicast delegate.
Jeśli delegat zwraca wartość (np. Func<string, int>), przy zwykłym wywołaniu (myDelegate("test")) zwrócony zostanie wynik tylko ostatniej metody w łańcuchu. Aby uzyskać wyniki wszystkich subskrybentów, trzeba iterować po liście wywołań przez GetInvocationList().


// Przykład iteracji wyników wszystkich subskrybentów
var list = myDelegate.GetInvocationList();
foreach (var d in list)
{
    var r = ((Func<string, int>)d)("test");
    Console.WriteLine(r);
}

Błąd nr 3: Przechwycenie zmiennej pętli w closure.
Klasyczna pułapka dla początkujących: lambda stworzona wewnątrz pętli for przechwytuje samą zmienną iteratora, a nie jej bieżącą wartość.


// Źle: wszystkie akcje wypiszą ostatnią wartość i
for (int i = 0; i < 3; i++) 
{
    actions[i] = () => Console.WriteLine(i);
}

// Dobrze: tworzymy lokalną kopię dla każdej iteracji
for (int i = 0; i < 3; i++) 
{
    int copy = i;
    actions[i] = () => Console.WriteLine(copy);
}

Błąd nr 4: Tworzenie wycieków pamięci.
Jeśli metoda instancji subscribuje się do delegata długowiecznego obiektu, ale się nie odsubskrybuje, powstaje wyciek pamięci. Długowieczny obiekt trzyma referencję do subskrybenta i garbage collector nie może go usunąć. Pilnuj odsubskrybowania, szczególnie w klasach o ograniczonym lifecycle (np. komponentach UI).

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