Rozdział 6. Delegaty i zdarzenia.
Adam Boduch
W poprzednim rozdziale powiedzieliśmy sobie sporo o programowaniu obiektowym. Jest to temat bardzo rozległy, szczególnie w kontekście programowania w języku C#, który jest całkowicie obiektowy. Nim przejdziemy do dalszego omawiania języka C#, chciałbym wspomnieć o dwóch mechanizmach, których wyjaśnienie musi znaleźć się w tej książce, mimo iż są one nieco bardziej zaawansowane. Mowa będzie o delegatach oraz zdarzeniach.
Na tym etapie nauki zapewne nie będziesz miał okazji lub potrzeby używania mechanizmów, o których traktuje ten rozdział. Jest to jednak rzecz warta uwagi i dlatego postanowiłem przeznaczyć kilka najbliższych stron na jej omówienie. Być może gdy osiągniesz wyższy poziom umiejętności programowania, dostrzeżesz zalety płynące z używania delegatów. Wówczas będziesz mógł powrócić do tego rozdziału, aby dowiedzieć się czegoś więcej na temat tych technik.
1 Delegaty
1.1 Tworzenie delegatów
1.2 Użycie delegatów
1.3 Funkcje zwrotne
1.4 Delegaty złożone
1.5 Metody anonimowe
2 Zdarzenia
3 Podsumowanie
Delegaty
Delegaty są często porównywane do wskaźników funkcji z języka C++. Faktem jest, że C# czerpie wiele z języka C++ i Javy, jednak wiele rzeczy jest moim zdaniem ulepszonych i prostszych w użyciu, tak jak delegaty.
Pamiętasz, jak opisywałem zjawisko polimorfizmu? Pisałem wówczas o późnym powiązaniu, o tym, iż decyzja, jaki obiekt ma zostać utworzony, może zapaść w trakcie działania programu. Teraz wyobraź sobie sytuację, w której nie jesteś pewien, jaka metoda ma zostać wykonana, decyzja ta zostanie podjęta w trakcie działania aplikacji, np. w zależności od decyzji użytkownika.
Np. użyty algorytm zostanie wybrany przez program w trakcie działania na podstawie zanalizowanych danych. Owszem — możesz wykorzystać instrukcję if
lub switch
, ale przy bardziej rozbudowanych aplikacjach praktyczniejsze będzie użycie delegatów.
Można powiedzieć, że delegaty przechowują referencję (adres w pamięci) do danych metod. Korzystając z delegatów, możesz wykonać kod znajdujący się w metodach.
Tworzenie delegatów
Delegaty to w rzeczywistości nowe typy danych. Mogą być zadeklarowane w klasie jako typ zagnieżdżony lub poza nią. Oto przykład:
public delegate int Foo(int X);
Tworzenie nowych delegatów jest proste, wygląda jak deklarowanie nowej metody. Jedyną różnicą jest słówko kluczowe delegate
.
Delegaty są nowymi typami danych. Aby je wykorzystać, tworzymy więc nowe zmienne wskazujące na te typy, do których możemy przypisać metody odpowiadające sygnaturze delegatu.
Delegaty nie posiadają implementacji, jedynie sygnaturę (nagłówek) zakończoną znakiem średnika.
Sygnaturę określają parametry delegatu oraz typ zwrotny.
To, że w poprzednim zdaniu napisałem o przypisywaniu metody do zmiennej, nie było pomyłką, o czym za chwilę się przekonasz. Ponieważ do zmiennej delegatu można przypisać jedynie metodę, która posiada taki sam nagłówek, należy ów metody wcześniej utworzyć:
static int Power(int X)
{
return X * X;
}
static int Div(int X)
{
return X / 2;
}
Jak widzisz, powyższe metody posiadają jeden parametr typu int
oraz zwracają wartość również w postaci typu int
, czyli identycznie jak nasz delegat Foo
.
Delegaty mogą zostać zainicjowane, zanim zostaną użyte, tak jak to ma miejsce w przypadku zwykłych klas. Oznacza to, że należy utworzyć obiekt delegatu i jako parametr podać w nim nazwę metody, na którą ma wskazywać:
Foo MyFoo = new Foo(Power);
Od teraz użycie MyFoo()
jest równoznaczne z wywołaniem metody Power()
(listing 6.1).
Istnieje również możliwość utworzenia zmiennej wskazującej na delegat i przypisanie do niej wartości:
Foo MyFoo = Power;
Listing 6.1. Przykład użycia delegatów
using System;
namespace DelegateApp
{
public delegate int Foo(int X);
class Program
{
static int Power(int X)
{
return X * X;
}
static int Div(int X)
{
return X / 2;
}
static void Main(string[] args)
{
Foo MyFoo = new Foo(Power);
Console.WriteLine("10x10 = " + MyFoo(10));
MyFoo = Div;
Console.WriteLine("10/2 = " + MyFoo(10));
Console.Read();
}
}
}
Zwróć uwagę na przypisanie:
MyFoo = Div;
Obiektowi MyFoo
przypisałem metodę Div()
. Oznacza to, iż od tej chwili użycie MyFoo()
równać się bedzie użyciu metody Div()
.
Użycie delegatów
Jak wspomniałem, przy pomocy delegatów możemy decydować w trakcie działania programu, jaka metoda zostanie wykonana. Rysunek 6.1 prezentuje prosty program wykorzystujący mechanizm delegatów.
Rysunek 6.1. Program prezentujący działanie delegatów
W tej prostej aplikacji użytkownik decyduje, jakie działanie ma zostać wykonane (mnożenie czy dzielenie). Dane wpisane w kontrolkach TextBox
są przekazywane do obiektu delegatu, który wykonuje odpowiednie działanie (listing 6.2).
Listing 6.2. Prezentacja działania delegatów
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace DelegateApp
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private int Multiple(int X, int Y)
{
return X * Y;
}
private int Div(int X, int Y)
{
return X / Y;
}
private void btnDoIt_Click(object sender, EventArgs e)
{
Math MyMath;
if (radioMultiple.Checked)
{
MyMath = new Math(Multiple);
}
else
{
MyMath = new Math(Div);
}
lblResult.Text = "Suma: " +
Convert.ToString(MyMath(Int32.Parse(textX.Text), Int32.Parse(textY.Text)));
lblResult.Visible = true;
}
}
public delegate int Math(int X, int Y);
}
Owszem, ktoś może powiedzieć, że to samo zadanie można zrealizować, używając instrukcji warunkowej:
Math MyMath;
int Result;
if (radioMultiple.Checked)
{
Result = Multiple(Int32.Parse(textX.Text), Int32.Parse(textY.Text));
}
else
{
Result = Div(Int32.Parse(textX.Text), Int32.Parse(textY.Text));
}
lblResult.Text = "Suma: " + Convert.ToString(Result);
Zgadza się, taki kod również jest poprawny, ale moim zdaniem przejrzystszy jest jednak ten z użyciem delegatów.
OK, zadanie z listingu 6.2 rzeczywiście można zrealizować przy pomocy instrukcji warunkowej, wywołując po prostu odpowiednią metodę. Delegaty mają jednak większe zastosowanie, o czym przekonasz się już za chwilę.
Funkcje zwrotne
Kolejnym zastosowaniem delegatów są funkcje zwrotne. Parametr danej metody może wskazywać na delegat. Spójrz na poniższy przykład:
static void Info(int X, int Y, Foo MyFoo)
{
MyFoo("Rezultat: " + (X + Y));
}
Konstrukcja jest dość dziwna, wywołujemy bowiem metodę MyFoo()
, która w rzeczywistości wskazuje na delegat:
public delegate void Foo(string Message);
W rzeczywistości więc wywołujemy metodę, do której odnosi się delegat. Aby lepiej to zrozumieć, spójrz na listing 6.3, gdzie umieściłem cały kod programu prezentujący zastosowanie funkcji zwrotnych.
Listing 6.3. Program prezentujący zastosowanie funkcji zwrotnych
using System;
using System.IO;
namespace DelegateApp
{
public delegate void Foo(string Message);
class Program
{
private static readonly bool Write = false;
static void OutputToConsole(string Message)
{
Console.WriteLine(Message);
}
static void OutputToFile(string Message)
{
// zapisywanie do pliku
}
static void Info(int X, int Y, Foo MyFoo)
{
MyFoo("Rezultat: " + (X + Y));
}
static void Main(string[] args)
{
Foo MyFoo;
if (Write)
{
MyFoo = OutputToFile;
}
else
{
MyFoo = OutputToConsole;
}
Info(2, 3, MyFoo);
Console.Read();
}
}
}
W metodzie Main()
zadeklarowałem zmienną wskazującą na delegat, do której przypisałem metodę, w zależności od wyniku instrukcji warunkowej. Następnie wywoływana zostaje metoda Info()
, w której w jednym z parametrów jest zmienna wskazująca na delegat.
Delegaty złożone
Bardzo fajną możliwością jest przypisywanie do delegatu więcej niż jednej metody. Wówczas przy wywołaniu delegatu wywołane zostaną wszystkie przypisane do niego metody.
Przypisywanie dodatkowych metod do delegatu jest bardzo proste. Można do tego wykorzystać operatory += oraz -=. Zmodyfikujmy nieco program z listingu 6.3:
Foo MyFoo;
MyFoo = OutputToConsole;
MyFoo += OutputToFile;
MyFoo("Hello World");
MyFoo -= OutputToFile;
MyFoo("Hello my darling!");
Możesz spróbować zmodyfikować program w ten sposób i sprawdzić jego działanie.
Metody anonimowe
Deklaracja delegatu to nie wszystko. Musimy również przypisać odpowiednią metodę, która ma być wywoływana po użyciu delegatu. Użycie metod anonimowych pozwala na przyspieszenie procesu projektowania poprzez pominięcie nazwy metody.
Posłużmy się poprzednim przykładem. Spójrz na poniższy kod:
Foo MyFoo = delegate(string M)
{
Console.WriteLine(M);
};
MyFoo("Hello World");
Do zmiennej MyFoo (typu Foo
) przypisujemy kod, który będzie wykonywany w momencie napotkania instrukcji MyFoo
. Tworzenie anonimowych metod przypomina tworzenie zwykłych. Różnicą jest to, że tutaj pomijana jest nazwa metody, a do jej tworzenia używane jest słowo kluczowe delegate
.
Zwróć uwagę na konieczność użycia znaku średnika po klamrze zamykającej kod metody anonimowej.
Zdarzenia
O zdarzeniach wspominałem już w poprzednim rozdziale. Myślę, że po dotychczasowej lekturze masz pojęcie o znaczeniu i zastosowaniu zdarzeń. Powinieneś również umieć wykorzystać zdarzenia, używając środowiska Visual C# Express Edition. Chciałbym na chwilę zatrzymać się przy zdarzeniach i opisać, jak wygląda ich działanie i wykorzystanie wewnątrz kodu.
Jak wiesz, zdarzenia używane są do oprogramowania czynności zachodzących w trakcie działania aplikacji. Takim zdarzeniem może być ruch myszą, kliknięcie w obrębie komponentu, przesunięcie kontrolki i wiele innych.
Utwórz nowy projekt aplikacji Windows Forms. Po załadowaniu niezbędnych elementów przez środowisko Visual C# Express Edition na formularzu umieść przykładowy komponent — np. Button
. Kliknij na niego podwójnie, aby wygenerować kod:
private void button1_Click(object sender, EventArgs e)
{
}
Nie zastanawia Cię, jak to się dzieje, że po naciśnięciu przycisku wykonywany jest kod znajdujący się w tej metodzie? Zdarzenia w dużej mierze opierają się na delegatach. W pliku *.designer.cs możesz odnaleźć instrukcje, które odpowiadają za utworzenie przycisku Button na formularzu oraz za przypisanie określonej metody do danego zdarzenia:
this.button1.Click += new System.EventHandler(this.button1_Click);
Zdarzenie Click
zadeklarowane jest w klasie Control
(znajdującej się w przestrzeni nazw System.Windows.Forms
) w sposób następujący:
public event EventHandler Click;
Delegaty deklarujemy z użyciem słowa kluczowego delegate
, a zdarzenia — z użyciem słowa event
. Fraza EventHandler
nie należy do słów kluczowych języka C#. Jest to nazwa delegatu określonego w przestrzeni System
:
public delegate void EventHandler(object sender, EventArgs e);
Innymi słowy, procedury zdarzeniowe (metody) przypisywane do zdarzenia Click
muszą mieć sygnaturę zgodną z delegatem EventHandler
. Pierwszym parametrem sygnatury delegatu jest parametr typu object
(mówiliśmy o tym w rozdziale 5.), a drugim — typu EventArgs
. Klasa EventArgs
nie robi nic konkretnego, właściwie jest jedynie klasą bazową dla innych, które przechowują informacje o zdarzeniu.
Skoro powiedzieliśmy, że zdarzenia w dużej mierze opierają się na delegatach, nic nie stoi na przeszkodzie, aby przypisać do danego zdarzenia więcej niż jedną metodę:
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Procedura zdarzeniowa #1");
}
private void myButton_Click(object sender, EventArgs e)
{
MessageBox.Show("Procedura zdarzeniowa #2");
}
private void Form1_Load(object sender, EventArgs e)
{
button1.Click += button1_Click;
button1.Click += myButton_Click;
}
Metoda Form1_Load()
jest w rzeczywistości procedurą zdarzeniową dla zdarzenia Load()
, które jest wywoływane w momencie ładowania formularza. W metodzie tej nakazujemy przypisać do zdarzenia Click
metody button1_Click()
oraz myButton_Click()
.
Więcej informacji na temat biblioteki WinForms oraz projektowania wizualnego znajdziesz w rozdziale 10.
Napiszmy prosty program obsługujący zdarzenia. Oczywiście najczęstszym zastosowaniem zdarzeń jest ich użycie wraz z komponentami, ale ja zaprezentuję prosty przykład. Klasa, która dokonuje banalnego działania: dzielenie dwóch liczb wskazanych przez użytkownika:
class DoIt
{
public delegate void OddDelegate(float Value);
public event OddDelegate Odd;
public float Div(float X, float Y)
{
float Z = X / Y;
if (Z % 2 == 0)
{
Odd(Z);
}
return Z;
}
}
W klasie zadeklarowałem delegat OddDelegate
oraz darzenie Odd
. W metodzie Div()
następuje dzielenie wartości wskazanych w parametrach X oraz Y. Następnie sprawdzamy, czy liczba jest parzysta. Jeżeli tak, wywołujemy zdarzenie Odd
.
Na formularzu umieściłem dwa komponenty TextBox
oraz przycisk. Oprogramowałem zdarzenie Click
przycisku w następujący sposób:
private void btnDoIt_Click(object sender, EventArgs e)
{
DoIt MyDoIt = new DoIt();
MyDoIt.Odd += new DoIt.OddDelegate(MyOdd);
float Result = MyDoIt.Div(float.Parse(edtX.Text), float.Parse(edtY.Text));
lblResult.Text = "Suma: " + Convert.ToString(Result);
lblResult.Visible = true; // spraw, aby komponent był widoczny
}
Na samym początku utworzyłem nowy egzemplarz (obiekt) klasy DoIt
. Następnie przypisałem metodę MyOdd()
do zdarzenia Odd
. Innymi słowy, jeżeli liczba uzyskana w wyniku dzielenia będzie parzysta, zostanie wywołana metoda MyOdd()
, której budowa jest dość prosta:
private void MyOdd(float Value)
{
MessageBox.Show("Liczba parzysta " + Value);
}
Działanie takiego programu prezentuje rysunek 6.2, a cały kod źródłowy zawarty jest na listingu 6.4.
Rysunek 6.2. Przykład użycia zdarzeń
Listing 6.4. Przykład wykorzystania zdarzeń
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace EventsApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void MyOdd(float Value)
{
MessageBox.Show("Liczba parzysta " + Value);
}
private void btnDoIt_Click(object sender, EventArgs e)
{
DoIt MyDoIt = new DoIt();
MyDoIt.Odd += new DoIt.OddDelegate(MyOdd);
float Result = MyDoIt.Div(float.Parse(edtX.Text), float.Parse(edtY.Text));
lblResult.Text = "Suma: " + Convert.ToString(Result);
lblResult.Visible = true;
}
}
class DoIt
{
public delegate void OddDelegate(float Value);
public event OddDelegate Odd;
public float Div(float X, float Y)
{
float Z = X / Y;
if (Z % 2 == 0)
{
Odd(Z);
}
return Z;
}
}
}
Podsumowanie
Delegaty i zdarzenia należą do bardziej skomplikowanych mechanizmów języka C#, na początku możesz nie dostrzegać zalet ich wykorzystania. Z czasem jednak, kiedy będziesz projektował bardziej zaawansowane aplikacje, delegaty mogą okazać się bardzo wydajnym mechanizmem zwiększającym czytelność i wydajność kodu. Ze zdarzeniami natomiast będziesz miał kontakt cały czas, programując z wykorzystaniem wizualnej biblioteki Windows Forms.
Mam pytanie. Czemu to jest 3 razy powtórzone?
Zmieniłem to, ale jeżeli to było specjalnie to sorry.