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.