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).
GO TO FULL VERSION