Wrocław z lotu ptaka

Wskazówka w języku C#: delegate

Są takie słowa kluczowe w języku C#, bez których można się obejść. Tylko po co się ograniczać, jeśli ich zrozumienie może zaoszczędzić nam czas, liczbę linii kodu do napisania… i wzbudzić zazdrość wśród Javowców 🙂 Zapraszam na trzy słowa o delegate.

Trochę SPAMu na początek

Wyobraźmy sobie, że stoimy przed zadaniem napisania aplikacji mającej na celu wysłanie określonej listy wiadomości mailowych. Wysłanie każdej wiadomości trwa pewien nieokreślony czas, zaś my chcielibyśmy być informowani na bieżąco o aktualnym postępie. Przykładowa implementacja funkcji spełniającej powyższe założenia mogłaby zostać napisana w następujący sposób:

public static void SendEmails(IList<Email> emails)
{
  Console.WriteLine("Sending emails started.");

  for (var i = 0; i < emails.Count; ++i)
  {
    EmailSender.Send(emails[i]);
        
    if (i % (emails.Count/20) == 0)
    {
      Console.WriteLine("Sending emails in progress... " +
                       $"{((double)i + 1) / emails.Count:P} sent.");
    }                
  }

  Console.WriteLine("Sending emails finished.");
}

Funkcja robi, to co założyliśmy. Informuje przez konsolę o postępie w wysyłaniu e-maili mniej więcej co 5%. Commitujemy, zamykamy laptopa, idziemy do domu. Następnego dnia przychodzi do nas szef i mówi:

Słuchaj, fajnie zrobiłeś tę funkcję wysyłającą e-maile. Tylko zmieniły się trochę wymagania. Mamy do wysłania 38 milionów wiadomości. W sumie, to nie chce mi się zerkać na ekran, żeby sprawdzać co chwilę jaki jest postęp. Zrób tak, żebym dostawał smsa co 5 procent. Albo nie, niech będzie co 10. A, i może lepiej, żebym dostawał maila z informacją o postępie. Albo może go tweetuj. Albo.. sam nie wiem, później zadecyduję. A, i wydziel to do jakiegoś modułu – przyda się też do aplikacji dla działu marketingu.

Olaboga – myślisz sobie. Jak to zaimplementować, żeby było przejrzyście i jednocześnie spełniało – notabene – niejasne wymagania? Z pomocą przychodzą delegaty.

Czym są delegaty?

Delegaty definiowane przez słowo kluczowe delegate mogą być rozumiane jako wskaźniki na funkcje, czy też pewne byty, do których można przypisać funkcje o określonej sygnaturze. Zobaczmy to na prostym przykładzie. Chcemy utworzyć delegatę, która określa funkcję przyjmującą liczbę typu int i zwracającą liczbę typu int. Kod tej deklaracji mógłby wyglądać tak:

delegate int SomeFunction(int someParameter);

Od tej pory SomeFunction możemy traktować jako typ i przypisywać do obiektu go reprezentującego dowolną metodę, która przyjmuje inta i zwraca inta. Mając zdefiniowaną funkcję:

public static int Fibonacci(int n)
{
  /*...*/
}

Możemy przypisać ją do obiektu naszego nowo-zdefiniowanego typu i przy jego pomocy ją wywołać:

SomeFunction someFunction = Fibonacci;
var result = someFunction(6);

Jak to może przydać się do rozwiązania naszego problemu wysyłania maili? Zobaczmy 🙂

Jeszcze więcej SPAMu

Szef wymaga, aby nasza funkcja wysyłająca maile miała możliwość raportowania bieżącego stanu, jednak w tym momencie nie wiadomo, czy raport będzie wysyłany smsem, mailem, Twitterem, czy jeszcze w inny sposób. Co wiemy? Wiemy, że mamy trzy typy raportów. Pierwszy to taki, który informuje o rozpoczęciu wysyłania wiadomości, drugi zdaje relację o postępie, trzeci zaś o zakończeniu. Idealnie sprawdzą się do tego następujące delegaty:

delegate void StatusFunction();
delegate void ProgressFunction(int performed, int toPerform);

Zobaczmy, jak zmieni się nasza funkcja do wysyłania maili po zastosowaniu zdefiniowanych delegat:

public static void SendEmails(
  IList<Email> emails, 
  StatusFunction start, 
  ProgressFunction reportProgress, 
  StatusFunction finish)
{
  start();

  for (var i = 0; i < emails.Count; ++i)
  {
    EmailSender.Send(emails[i]);

    reportProgress(i + 1, emails.Count);
  }

  finish();
}

Aby funkcja zachowywała się w taki sposób jak przed użyciem delegat, należy zaimplementować funkcje raportujące:

public static void Start()
{
  Console.WriteLine("Sending emails started.");
}

public static void Progress(int performed, int toPerform)
{
  if (performed % (toPerform / 20) == 0)
  {
    Console.WriteLine("Sending emails in progress... " +
                     $"{(double)performed / toPerform:P} sent.");
  }
}

public static void Stop()
{
  Console.WriteLine("Sending emails finished.");
}

Od teraz wysyłanie maili to proste wywołanie naszej funkcji w taki sposób:

SendEmails(emails, Start, Progress, Stop);

Pozostaje tylko wydzielić ją ładnie do jakiejś klasy i zamknąć w assembly. Chcemy raportować postęp w inny sposób? Wysyłać smsem? Mailem? Informować o każdej wysłanej wiadomości? Proszę bardzo, zmieniamy tylko implementację funkcji Start, Progress i Stop.

Kilka uwag na zakończenie

1. Można definiować delegaty generyczne

Na przykład:

public delegate T DoSomeWork<T>(T x);

DoSomeWork będzie pasować do każdej funkcji, która zwraca wartość tego samego typu, co przyjmuje.

2. W języku C# są już zdefiniowane podstawowe delegaty

Action pasuje do nic nie zwracającej metody bezparametrowej. Action<T1, T2, …, TN> pasuje do nic nie zwracającej metody, ale przyjmującej parametry typów określonych przez T, zaś Func<T1, T2, …, TN> pasuje do metody, dla której pierwsze typy wyznaczają parametry wejściowe, zaś ostatni – typ wartości zwracanej. Dla przykładu, nasza funkcja do wysyłania e-maili po zastosowaniu delegatów wbudowanych w język wyglądałaby tak:

public static void SendEmails(
  IList<Email> emails,
  Action start,
  Action<int, int> reportProgress,
  Action finish)
{
  /*...*/	    
}

3. Obiekt delegaty może zawierać więcej niż jedną funkcję

Przykład:

public static void DoSomething() { }
public static void DoSomethingElse() { }

/*...*/

  Action someAction = DoSomething;
  someAction += DoSomethingElse;
  
  someAction();

/*...*/

W powyższym przykładzie someAction() spowoduje wywołanie obydwu metod. Gdyby zwracały one jakąś wartość, wynikiem wywołania byłby rezultat ostatniej z nich. Analogicznie, oprócz dodawania funkcji do obiektu delegaty, można je również odejmować operatorem „-=”.

Poniżej znajduje się link afiliacyjny do serwisu ceneo.pl.
Jeżeli przy jego pomocy przejdziesz na stronę sklepu, otrzymam za to prowizję.

Przygotowując niniejszy wpis posiłkowałem się wiedzą z książki:

Joseph Albahari, Ben Albahari, „C# 6.0 w pigułce”, Helion 2016

  • Maciek

    Zastanawialem sie jak rozwiazal bym ten problem. Doszedlem do wniosku, ze pewnie stworzylbym interfejs Sender, nastepnie zaleznie od konfiguracji, skorzystalbym z odpowiedniej klasy implementujacej ten interfejs. Czyli pewnie TweeterSender, EmailSender, SMSSender. W czym jest lepsze uzycie delegat w tym przypadku? jest szybsze?

    • Maciek

      ew zrobilbym IProgress IStart IProgress. Nastepnie dodalbym klasy typu TweeterManager EmailManager SMSManager, z ktorych kazda implementowalaby te interfejsy. Delegaty wydaja mi sie bardziej eleganckim i szybkim rozwiazaniem, ale moze maja jeszcze jakies plusy, ktorych tu nie zauwazylem

    • Słuszna uwaga – delegaty swobodnie można zastąpić odpowiednim użyciem interfejsów. I jedno i drugie podejście ma swoje wady i zalety. Jakie są plusy użycia delegatów w tym przypadku? Nie musimy tworzyć całej obudowy w postaci interfejsów, dziedziczenia, implementacji – po prostu wywołujemy metodę używając dowolnych funkcji pasujących do delegat. Po drugie, możemy wywołać metodę z obiektem delegaty zawierającym dowolną liczbę funkcji – np. chcemy, aby informowanie o postępach szło jednocześnie kilkoma kanałami. No a jaki jest minus? Skoro nie jesteśmy zmuszeni do przemyślenia architektury – ryzyko powstania spaghetti code rośnie 🙂

  • HeniekP

    Oprócz Action i Func jest jeszcze Predicate.

    • Sure, Predicate to nic innego jak Func. Warto wspomnieć, że ma ciut dłuższą historę od Action i Func, bowiem delegata ta pojawiła się już w .net 2.0 na potrzeby kolekcji, zaś Action i Func zostały wprowadzone dopiero w wersji 3.5.

  • mw

    A co powiecie na dodanie eventow przy klasie wysylajacej? Nie musze wtedy przekazywac callbackow, jesli nie jestem zainteresowany.

    • Radosław Maziarka

      Mi się podoba taki pomysł – delegujemy odpowiedzialność za obsługę zdarzeń na zewnątrz. Kod w środku jest czystszy i mamy większą rozszerzalność.