17. Delegaty i zdarzenia

17. Delegaty i zdarzenia

Autor: Paweł Kruczkowski

Opublikowano: 1/16/2007, 12:00 AM

Liczba odsłon: 183148

Tematem niniejszego artykułu są delegaty i zdarzenia. Są to dwa ściśle ze sobą powiązane pojęcia, o których warto parę słów napisać.

Najłatwiej wprowadzić się w świat delegatów wyobrażając sobie prezydenta Polski, który z braku czasu nie może osobiście uczestniczyć w uroczystościach prezydenta Stanów Zjednoczonych, pomimo faktu, że został on na nie zaproszony. Wówczas prezydent Polski wysyła do swojego kolegi zza oceanu kogoś upoważnionego (np. premiera). Nadaje mu pewne prawa (premier ma obowiązek reprezentować Polskę), nakazuje mu przekazać ciepłe słowa w postaci podziękowania za zaproszenie i…przeproszenia za brak udziału J (będą to parametry delegata) oraz  oczekuje, że prezydent Stanów Zjednoczonych będzie jednak zadowolony z obecności „tylko” premiera znad Wisły.

W takiej sytuacji – premier Polski jest delegatem.

Bardzo często spotkamy się w naszych programach z sytuacją, w której nasz program wykonuje jakieś działanie, ale nie wie jakich obiektów a nawet metod ma w tym celu użyć. Na przykład: naciśnięcie przycisku ma poinformować inny obiekt, że przycisk został przyciśnięty. Ale jaki to obiekt? Nie wiadomo, dlatego najlepszym rozwiązaniem jest połączenie tego przycisku z delegatem, który następnie w czasie wykonywania się programu wywoła odpowiednią metodę.

Wiemy już mniej więcej do czego służą delegaty, ale tematem dzisiejszego artykułu są również zdarzenia. Nie jest to przypadkowe, bowiem są one często właśnie razem z delegatami spotykane w programach. Możemy powiedzieć więcej, delegaty i zdarzenia są ściśle powiązane ze sobą, ponieważ delegat  potrafi obsługiwać zdarzenie.

A co to jest zdarzenie? Zdarzenie to pojęcie określające, że „coś się wydarzyło” w naszym programie, np. kliknięcie przycisku jest chyba najprostszym zdarzeniem jakie możemy osiągnąć w naszej aplikacji.

Delegaty to obiekty, w pełni obsługiwane przez język C# 2.0. Z punktu widzenia programisty delegat to typ referencyjny, który stanowi interfejs metody o odpowiedniej sygnaturze oraz zwracanym typie. Poniżej prezentujemy sposób deklarowania delegatów:

public delegate string MojDelegat(object mojObjekt1, object MojObject2);

A więc delegat tworzymy używając słowa kluczowego delegate, po którym znajduje się sygnatura metod, których interfejsem może być dany delegat.

W powyższym fragmencie kodu utworzyliśmy więc delegat o nazwie MojDelegat, który może zawierać dowolną metodę przyjmującą jako parametry 2 obiekty i zwracającą ciąg łańcuchów (string).

Do zapamiętania: delegat używamy do wywołania metody, którą on zawiera. Nieistotne jest, czy w danej chwili delegat zawiera metodę składową (tworzymy następnie egzemplarz tej metody, która zwraca odpowiedni typ i ma odpowiednią sygnaturę), czy używa metod anonimowych (o nich w niniejszym artykule też napiszemy parę słów) - istotne bowiem, że delegat metody te potrafi wywołać.

Napiszmy więc pierwszy przykład, który będzie prezentować sposób używania delegatów:

public class Delegaty
{
    public delegate int MojDelegat(int a, int b);         
   
    public int Dodaj(int a, int b)
    {
        return a + b;
    }
 
    public int Odejmij(int a, int b)
    {
        return a - b;
    }
 
    public int Pomnoz(int a, int b)
    {
        return a * b;
    }
 
    public int Podziel(int a, int b)
    {
        return a / b;
    }
}
 
class Glowna
{
    static void Main() 
    {
        Delegaty d = new Delegaty();
       

  Delegaty.MojDelegat dodawanie = new Delegaty.MojDelegat(d.Dodaj);
        int wynikDodawania = dodawanie(4, 6);
        System.Console.WriteLine("Wynik dodawania wynosi: {0}.", wynikDodawania.ToString());
 
        Delegaty.MojDelegat odejmowanie = new Delegaty.MojDelegat(d.Odejmij);
        int wynikOdejmowania = odejmowanie(22, 11);

  System.Console.WriteLine("Wynik odejmowania wynosi: {0}.",   

  wynikOdejmowania.ToString());
 
        Delegaty.MojDelegat mnozenie = new Delegaty.MojDelegat(d.Pomnoz);
        int wynikMnozenia = mnozenie(3, 8);
        System.Console.WriteLine("Wynik mnożenia wynosi: {0}.", wynikMnozenia.ToString());
 
        Delegaty.MojDelegat dzielenie = new Delegaty.MojDelegat(d.Podziel);
        int wynikDzielenia = dzielenie(64, 8);
        System.Console.WriteLine("Wynik dzielenia wynosi: {0}.", wynikDzielenia.ToString());
    }
}
W powyższym przykładzie zdefiniowaliśmy delegat o nazwie MojDelegat, który może zawierać dowolną metodę, która musi spełnić 2 warunki (de facto określone właśnie przez definicję delegata):

  • Metoda ta musi przyjmować dokładnie 2 parametry, które muszą być liczbami całkowitymi
  • Metoda ta musi zwracać typ, który jest liczbą całkowitą (intiger).

Delegat taki zdefiniowaliśmy więc w następujący sposób:

      public delegate int MojDelegat(int a, int b);        i teraz możemy go już użyć, aby wywołał metody spełniające powyższe warunki.

W powyższym programie MojDelegat zawiera metodę dodawania dwóch liczb całkowitych (Dodaj()), metodę odejmowania dwóch liczb całkowitych(Odejmij()), metodę mnożenia przez siebie dwóch liczb całkowitych (Pomnoz()) oraz metodę potrafiącą podzielić dwie liczby całkowite (Podziel()). Oczywiście wszystkie te metody przyjmują dwa parametry i zwracają – zgodnie z definicją delegata – liczbę całkowitą. Dlatego też  w głównej klasie w statycznej metodzie Main(), delegat MojDelegat wywołuje te metody, gdyż spełniają jego warunki.

Prześledźmy więc sposób wywołania metody Dodaj():

Delegaty.MojDelegat dodawanie = new Delegaty.MojDelegat(d.Dodaj);

W powyższej linii kodu tworzymy egzemplarz delegata, a w jego konstruktorze przekazujemy metodę Dodaj() (wywołujemy ją na obiekcie d typu Delegaty). Następnie pod zmienną typu liczby całkowitej podstawiamy odpowiedni wynik uzyskany po wywołaniu metody Dodaj() z dwoma parametrami (odpowiednio: 4 i 6) przez nasz MojDelegat:

int wynikDodawania = dodawanie(4, 6);

W podobny sposób wywołujemy za pomocą delegata pozostałe metody zdefiniowane w klasie Delegaty. Po skompilowaniu i uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

Potrafimy więc już wywoływać za pomocą delegata metody, które on zawiera i które spełniają jego warunki. Następnym punktem dzisiejszego artykułu jest przybliżenie sposobu obsługiwania zdarzeń w języku C# 2.0 właśnie poprzez delegaty. Każdy z nas po lekturze wielu artykułów na temat ASP 2.0 na portalu CentrumXp.pl potrafi bez chwili zastanowienia się wymieć zdarzenia, jakie mogą zachodzić na stronie webowej. Najlepszym przykładem jest niewątpliwie kliknięcie jakiegoś przycisku, który natychmiast np. przeładowuje formę webową i wykonuje jakąś logikę biznesową. Mówimy wówczas, że kliknięcie tego przycisku jest zdarzeniem, jakie zostało właśnie wywołane.

W języku C# każdy obiekt może publikować zestaw zdarzeń, które następnie mogą być subskrybowane. Innymi słowy, klasa, w której definiowane jest zdarzenie nazywamy klasą publikującą, a wszystkie inne klasy, które zostały poinformowane o tym zdarzeniu są klasami subskrybującymi. A kto jest najlepszym łącznikiem – informatorem pomiędzy klasą publikującą a subskrybującą? Oczywiście, że dobrze już nam znane delegaty.

Delegaty są definiowane w klasie publikującej, natomiast w klasie subskrybującej tworzymy metodę, która pasuje do sygnatury delegata oraz deklarujemy egzemplarz typu tegoż delegata, wywołujący właśnie tę metodę. I w momencie zgłoszenia zdarzenia, metody klasy subskrybującej (metody obsługujące zdarzenie) zostaną wywołane przez delegata.

Metody obsługujące zdarzenie często nazywane są uchwytem zdarzenia. Zwykle metody te zwracają typ void i przyjmują dwa parametry: źródło zdarzenia (obiekt publikujący) oraz obiekt pochodny od klasy EventArgs (jest to klasa bazowa przechowująca wszystkie informacje o zdarzeniach).

Zobaczmy w poniższym fragmencie kodu, w jaki sposób programowo zadeklarować delegaty oraz zdarzenia i w jaki sposób je wiązać ze sobą:

    public delegate void EventHandler(object sender, EventArgs e);
 
    public class Przycisk
    {
        public event EventHandler Klikniecie;

    }

W powyższym fragmencie kodu zadeklarowaliśmy delegata o nazwie EventHandler, który może zawierać dowolną metodę, która – jak już wiemy – musi spełniać 2 warunki: w naszym przypadku musi przyjmować 2 parametry (źródło zdarzenia, czyli obiekt publikujący oraz obiekt pochodny od klasy EventArgs) oraz zwracać typ void.  Natomiast klasa Przycisk definiuje zdarzenie Klikniecie (zdarzenia definiujemy za pomocą słowa kluczowego: event), które jest obsługiwane przez delegata typu EventHandler. Słowo kluczowe event informuje kompilator, że delegat ten może być wywołany tylko przez klasę, która go definiuje, a inne klasy mogą ten delegat jedynie subskrybować lub rezygnować z subskrybcji. Takie podejście do deklarowania delegatów oraz zdarzeń jest jak najbardziej podejściem obiektowym i ukazuje cel użycia słowa event, czyli po prostu utworzenia zdarzenia zgłaszanego przez obiekt, na które reagować mogą inne obiekty.

W klasie Przycisk zdarzenie Klikniecie odpowiada ściśle polu prywatnemu typu EventHandler. Natomiast poza tą klasą, zdarzenie to może być użyte tylko z lewej strony operatorów „+=” (instalator obsługi zdarzeń) i „-=” (deinstalator obsługi zdarzeń).

Poniższy fragment kodu prezentuje sposób instalacji obsługi zdarzenia zdefiniowanego w klasie Przycisk:

public class MojaKlasa
      {
          Przycisk1.Klikniecie += new EventHandler(Przycisk1_Klikniecie);

      }

 
      void Przycisk1_Klikniecie(object sender, EventArgs e)
      {
            Console.WriteLine("Przycisk został kliknięty");

      }

W klasie MojaKlasa zainstalowaliśmy zdarzenie Przycisk1_Klikniecie dla zdarzenia Klikniecie w klasie Przycisk1. Innymi słowy, tworzymy egzemplarz typu delegata EventHandler, który przyjmuje metodę obsługi zdarzenia (metodę: Przycisk1_Klikniecie). Następnie kompilator rejestruje tego delegata (wspomniany obiekt klasy EventHandler) wiążąc go ze zdarzeniem Klikniecie.

Metoda Przycisk1_Klikniecie została wywołana przez delegata EventHandler, a więc jak widzimy jest typu void oraz przyjmuje 2 parametry (żródło zdarzenia oraz obiekt pochodny od bazowego EventArgs).

Poniższy przykład na pewno rozjaśni nam sposób używania delegatów oraz zdarzeń:

public class KlasaPublikujaca
{
    //definicja delegata
    public delegate void MojDelegat(int liczba);
 
    //definicja zdarzenia, ktore jest obslugiwane przez delegata MojDelegat
    public event MojDelegat MojeZdarzenie;
       
    //metoda dodajaca dwie liczby calkowite
    public int Dodaj(int a, int b)
    {
        //jesli warunek spelniny to zachodzi zdarzenie MojeZdarzenie
        if (a + b > 50)
        {
            MojeZdarzenie(a + b);
        }
 
        return a + b;           
    }
 
}
 
class GlownaKlasa
{
    public static void Main()
    {
        KlasaPublikujaca kp = new KlasaPublikujaca();
 
        kp.MojeZdarzenie += new KlasaPublikujaca.MojDelegat(kp_MojeZdarzenie);
 
        Console.WriteLine("Wynik sumy to: {0}", kp.Dodaj(44, 88));
    }
 
    static void kp_MojeZdarzenie(int liczba)
    {
       Console.WriteLine("Wynik otrzymaliśmy dzięki delegatowi. Suma wynosi: {0}.", liczba.ToString());
    }

}

W klasie publikującej zdefiniowaliśmy delegata MojDelegat, który będzie stanowił interfejs metod, które w swojej definicji będą przyjmować jeden parametr (liczba całkowita) oraz nie będą zwracać żadnych wartości. W klasie tej zadeklarowaliśmy również zdarzenie MojeZdarzenie, które będzie obsługiwane właśnie przez wspomnianego delegata. KlasaPublikujaca definiuje również metodę Dodaj(), która zwraca sumę dwóch liczb całkowitych. Jeśli ta suma jest większa od 50, to zostanie wywołane zdarzenie MojeZdarzenie i natychmiast klasa główna naszego programu zostanie o tym poinformowana. Odpowiada za to poniższy fragment kodu:

kp.MojeZdarzenie += new KlasaPublikujaca.MojDelegat(kp_MojeZdarzenie);

W takiej sytuacji tworzony jest egzemplarz delegata typu MojDelegat, który również przyjmuje metodę obsługi tego zdarzenia. Metoda kp_MojeZdarzenie(), bo o niej mowa, musi oczywiście pasować do sygnatury delegata MojDelegat oraz zwracać odpowiedni typ. Jest więc ona wywoływana przez delegata w momencie zajścia zdarzenia MojeZdarzenie w klasie publikującej (a więc, gdy suma dwóch liczb jest większa od 50).

Dlatego też w wyniku skompilowania i uruchomienia powyższego programu otrzymamy następujące wyniki:

W powyższym przykładzie subskrybowanie zdarzenia odbywa się w wyniku wywołania nowego egzemplarza delegata i przekazania nazwy metody, która obsługuje to zdarzenie:

MojeZdarzenie += new KlasaPublikujaca.MojDelegat(kp_MojeZdarzenie);

Powyższy fragment kodu możemy jednak zapisać inaczej, używając tzw. anonimowych metod. Metody takie pozwalają przekazać blok kodu zamiast nazwy metody. Takie podejście sprawia, że nasz kod jest bardziej wydajny i przede wszystkim czytelniejszy, a metoda anonimowa ma dostęp do wszystkich zmiennych w zasięgu danej definicji.

Poniższy fragment kodu prezentuje sposób używania metod anonimowych:

kp.MojeZdarzenie += delegate (int liczba)
      {
          Console.WriteLine("Wynik otrzymaliśmy dzięki delegatowi. Suma wynosi: {0}.", liczba.ToString());

      };

Jak łatwo zauważyć tworzenie nowego egzemplarza delegata zastąpione jest tutaj słowem: delegate, po którym następują parametry przekazywane do metody. Następnie w nawiasach okrągłych umieszczamy ciało naszej metody, a całość kończy się średnikiem.

Poniższy program zwraca identyczne wyniki co poprzedni, ale został napisany przy użyciu właśnie metody anonimowej:

public class KlasaPublikujaca
{
    //definicja delegata
    public delegate void MojDelegat(int liczba);
 
    //definicja zdarzenia, ktore jest obslugiwane przez delegata MojDelegat
    public event MojDelegat MojeZdarzenie;
       
    //metoda dodajaca dwie liczby calkowite
    public int Dodaj(int a, int b)
    {
        //jesli warunek spelniny to zachodzi zdarzenie MojeZdarzenie
        if (a + b > 50)
        {
            MojeZdarzenie(a + b);
        }
 
        return a + b;            
    }
 
}
class GlownaKlasa
{
    public static void Main()
    {
        KlasaPublikujaca kp = new KlasaPublikujaca();
 
        kp.MojeZdarzenie += delegate (int liczba)
        {
            Console.WriteLine("Wynik otrzymaliśmy dzięki delegatowi. Suma wynosi: {0}.", liczba.ToString());
        };
 
        Console.WriteLine("Wynik sumy to: {0}", kp.Dodaj(44, 88));
    }
             

}

Celem niniejszego artykułu było przybliżenie podstawowych informacji na temat delegatów oraz zdarzeń w języku C# 2.0. Zdefiniowaliśmy pojęcie klas publikujących oraz subskrybujących i na podstawie przykładów ukazaliśmy sposób obsługi zdarzeń definiowanych w klasach publikujących za pomocą delegatów.

Za tydzień opowiemy sobie o operacjach wejścia – wyjścia.